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" ;
1819import copy from "clipboard-copy" ;
20+ import FileSaver from "file-saver" ;
21+ import { json2csv } from "json-2-csv" ;
22+ import get from "lodash/get" ;
1923import isBoolean from "lodash/isBoolean" ;
2024import isFunction from "lodash/isFunction" ;
2125import 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" ;
2438import { Trace } from "@/types/traces" ;
2539import { Filter } from "@/types/filters" ;
2640import { formatDate , formatDuration } from "@/lib/date" ;
@@ -72,6 +86,8 @@ import { WORKSPACE_PREFERENCE_TYPE } from "@/components/pages/ConfigurationPage/
7286import { WORKSPACE_PREFERENCES_QUERY_PARAMS } from "@/components/pages/ConfigurationPage/WorkspacePreferencesTab/constants" ;
7387import AddToDropdown from "@/components/pages-shared/traces/AddToDropdown/AddToDropdown" ;
7488import ConfigurableFeedbackScoreTable from "../TraceDetailsPanel/TraceDataViewer/FeedbackScoreTable/ConfigurableFeedbackScoreTable" ;
89+ import { useIsFeatureEnabled } from "@/components/feature-toggles-provider" ;
90+ import { FeatureToggleKeys } from "@/types/feature-toggles" ;
7591
7692type 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