Skip to content

Commit 928930e

Browse files
aadereikoaadereiko
andauthored
[OPIK-3192] [FE]: add export buttons to details actions (#4173)
* [OPIK-3192]: add export buttons to details; * lint issues; * [OPIK-3192]: take all columns, not only selected; * add a chart conversation to threads; * propagate the change with export to all tracedetailspanel; * hardcode columns for exporting; * eslint issues; * add a FT support; * fix a bug with tooltips; * change labels; --------- Co-authored-by: aadereiko <[email protected]>
1 parent a697932 commit 928930e

File tree

5 files changed

+401
-54
lines changed

5 files changed

+401
-54
lines changed

apps/opik-frontend/src/components/pages-shared/traces/ThreadDetailsPanel/ThreadDetailsPanel.tsx

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Clock,
88
Coins,
99
Copy,
10+
Download,
1011
Hash,
1112
MessageCircleMore,
1213
MessageCircleOff,
@@ -16,11 +17,24 @@ import {
1617
Trash,
1718
} from "lucide-react";
1819
import copy from "clipboard-copy";
20+
import FileSaver from "file-saver";
21+
import { json2csv } from "json-2-csv";
22+
import get from "lodash/get";
1923
import isBoolean from "lodash/isBoolean";
2024
import isFunction from "lodash/isFunction";
2125
import isUndefined from "lodash/isUndefined";
26+
import uniq from "lodash/uniq";
2227

23-
import { COLUMN_TYPE, OnChangeFn } from "@/types/shared";
28+
import {
29+
COLUMN_FEEDBACK_SCORES_ID,
30+
COLUMN_TYPE,
31+
OnChangeFn,
32+
} from "@/types/shared";
33+
import {
34+
mapRowDataForExport,
35+
TRACE_EXPORT_COLUMNS,
36+
THREAD_EXPORT_COLUMNS,
37+
} from "@/lib/traces/exportUtils";
2438
import { Trace } from "@/types/traces";
2539
import { Filter } from "@/types/filters";
2640
import { formatDate, formatDuration } from "@/lib/date";
@@ -72,6 +86,8 @@ import { WORKSPACE_PREFERENCE_TYPE } from "@/components/pages/ConfigurationPage/
7286
import { WORKSPACE_PREFERENCES_QUERY_PARAMS } from "@/components/pages/ConfigurationPage/WorkspacePreferencesTab/constants";
7387
import AddToDropdown from "@/components/pages-shared/traces/AddToDropdown/AddToDropdown";
7488
import ConfigurableFeedbackScoreTable from "../TraceDetailsPanel/TraceDataViewer/FeedbackScoreTable/ConfigurableFeedbackScoreTable";
89+
import { useIsFeatureEnabled } from "@/components/feature-toggles-provider";
90+
import { FeatureToggleKeys } from "@/types/feature-toggles";
7591

7692
type ThreadDetailsPanelProps = {
7793
projectId: string;
@@ -117,6 +133,8 @@ const ThreadDetailsPanel: React.FC<ThreadDetailsPanelProps> = ({
117133
const [activeSection, setActiveSection] =
118134
useDetailsActionSectionState("lastThreadSection");
119135

136+
const isExportEnabled = useIsFeatureEnabled(FeatureToggleKeys.EXPORT_ENABLED);
137+
120138
const { mutate: threadFeedbackScoreDelete } =
121139
useThreadFeedbackScoreDeleteMutation();
122140
const [activeTab = DEFAULT_TAB, setActiveTab] = useQueryParam(
@@ -231,6 +249,105 @@ const ThreadDetailsPanel: React.FC<ThreadDetailsPanelProps> = ({
231249
});
232250
};
233251

252+
const exportColumns = useMemo(() => {
253+
const feedbackScoreNames = uniq(
254+
(thread?.feedback_scores ?? []).map(
255+
(score) => `${COLUMN_FEEDBACK_SCORES_ID}.${score.name}`,
256+
),
257+
);
258+
259+
return [...THREAD_EXPORT_COLUMNS, ...feedbackScoreNames];
260+
}, [thread]);
261+
262+
const traceExportColumns = useMemo(() => {
263+
const feedbackScoreNames = uniq(
264+
traces.reduce<string[]>((acc, trace) => {
265+
return acc.concat(
266+
(trace.feedback_scores ?? []).map(
267+
(score) => `${COLUMN_FEEDBACK_SCORES_ID}.${score.name}`,
268+
),
269+
);
270+
}, []),
271+
);
272+
273+
return [...TRACE_EXPORT_COLUMNS, ...feedbackScoreNames];
274+
}, [traces]);
275+
276+
const handleExportCSV = useCallback(async () => {
277+
try {
278+
if (!thread) return;
279+
280+
const mappedData = await mapRowDataForExport([thread], exportColumns);
281+
const mappedTraces = await mapRowDataForExport(
282+
traces,
283+
traceExportColumns,
284+
);
285+
286+
const dataWithConversationHistory = [
287+
{
288+
...mappedData[0],
289+
conversation_history: JSON.stringify(mappedTraces),
290+
},
291+
];
292+
293+
const csv = json2csv(dataWithConversationHistory);
294+
const fileName = `${threadId}-thread.csv`;
295+
const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
296+
FileSaver.saveAs(blob, fileName);
297+
298+
toast({
299+
title: "Export successful",
300+
description: "Exported thread to CSV",
301+
});
302+
} catch (error) {
303+
toast({
304+
title: "Export failed",
305+
description: get(error, "message", "Failed to export"),
306+
variant: "destructive",
307+
});
308+
}
309+
}, [thread, threadId, exportColumns, traces, traceExportColumns]);
310+
311+
const handleExportJSON = useCallback(async () => {
312+
try {
313+
if (!thread) return;
314+
315+
const mappedThreadData = await mapRowDataForExport(
316+
[thread],
317+
exportColumns,
318+
);
319+
const mappedTraces = await mapRowDataForExport(
320+
traces,
321+
traceExportColumns,
322+
);
323+
324+
const dataWithConversationHistory = {
325+
...mappedThreadData[0],
326+
conversation_history: mappedTraces,
327+
};
328+
329+
const fileName = `${threadId}-thread.json`;
330+
const blob = new Blob(
331+
[JSON.stringify(dataWithConversationHistory, null, 2)],
332+
{
333+
type: "application/json;charset=utf-8",
334+
},
335+
);
336+
FileSaver.saveAs(blob, fileName);
337+
338+
toast({
339+
title: "Export successful",
340+
description: "Exported thread to JSON",
341+
});
342+
} catch (error) {
343+
toast({
344+
title: "Export failed",
345+
description: get(error, "message", "Failed to export"),
346+
variant: "destructive",
347+
});
348+
}
349+
}, [thread, threadId, exportColumns, traces, traceExportColumns]);
350+
234351
const horizontalNavigation = useMemo(
235352
() =>
236353
isBoolean(hasNextRow) &&
@@ -573,6 +690,49 @@ const ThreadDetailsPanel: React.FC<ThreadDetailsPanelProps> = ({
573690
</DropdownMenuItem>
574691
</TooltipWrapper>
575692
<DropdownMenuSeparator />
693+
{!isExportEnabled ? (
694+
<TooltipWrapper
695+
content="Export functionality is disabled for this installation"
696+
side="left"
697+
>
698+
<div>
699+
<DropdownMenuItem
700+
onClick={handleExportCSV}
701+
disabled={!isExportEnabled}
702+
>
703+
<Download className="mr-2 size-4" />
704+
Export as CSV
705+
</DropdownMenuItem>
706+
</div>
707+
</TooltipWrapper>
708+
) : (
709+
<DropdownMenuItem onClick={handleExportCSV}>
710+
<Download className="mr-2 size-4" />
711+
Export as CSV
712+
</DropdownMenuItem>
713+
)}
714+
{!isExportEnabled ? (
715+
<TooltipWrapper
716+
content="Export functionality is disabled for this installation"
717+
side="left"
718+
>
719+
<div>
720+
<DropdownMenuItem
721+
onClick={handleExportJSON}
722+
disabled={!isExportEnabled}
723+
>
724+
<Download className="mr-2 size-4" />
725+
Export as JSON
726+
</DropdownMenuItem>
727+
</div>
728+
</TooltipWrapper>
729+
) : (
730+
<DropdownMenuItem onClick={handleExportJSON}>
731+
<Download className="mr-2 size-4" />
732+
Export as JSON
733+
</DropdownMenuItem>
734+
)}
735+
<DropdownMenuSeparator />
576736
<DropdownMenuItem onClick={() => setPopupOpen(true)}>
577737
<Trash className="mr-2 size-4" />
578738
Delete

0 commit comments

Comments
 (0)