diff --git a/front/components/assistant_builder/AssistantBuilder.tsx b/front/components/assistant_builder/AssistantBuilder.tsx index b52d4be3a214..01ec5f353917 100644 --- a/front/components/assistant_builder/AssistantBuilder.tsx +++ b/front/components/assistant_builder/AssistantBuilder.tsx @@ -1,6 +1,7 @@ import "react-image-crop/dist/ReactCrop.css"; import { + BarChartIcon, Button, ChatBubbleBottomCenterTextIcon, ChevronLeftIcon, @@ -500,6 +501,7 @@ export default function AssistantBuilder({ } buttonsRightPanel={ <> + {/* Chevron button */} - {rightPanelStatus.tab === null && template === null && ( - openRightPanelTab("Preview")} - size="md" - tooltip={ - isBuilderStateEmpty - ? "Add instructions or tools to Preview" - : "Preview" - } - variant="highlight" - disabled={isBuilderStateEmpty} - className={cn( - isPreviewButtonAnimating && "animate-breathing-scale" - )} - /> - )} - {rightPanelStatus.tab === null && template !== null && ( + {rightPanelStatus.tab === null && ( + {/* Preview Button */} openRightPanelTab("Preview")} size="sm" variant="outline" tooltip="Preview your assistant" + className={cn( + isPreviewButtonAnimating && "animate-breathing-scale" + )} + disabled={isBuilderStateEmpty} /> - openRightPanelTab("Template")} - size="sm" - variant="outline" - tooltip="Template instructions" - /> + {/* Performance Button */} + {!!agentConfigurationId && ( + openRightPanelTab("Performance")} + size="sm" + variant="outline" + tooltip="Inspect feedback and performance" + /> + )} + {/* Template Button */} + {template !== null && ( + openRightPanelTab("Template")} + size="sm" + variant="outline" + tooltip="Template instructions" + /> + )} )} > @@ -565,6 +568,7 @@ export default function AssistantBuilder({ rightPanelStatus={rightPanelStatus} openRightPanelTab={openRightPanelTab} builderState={builderState} + agentConfigurationId={agentConfigurationId} setAction={setAction} /> } diff --git a/front/components/assistant_builder/AssistantBuilderPreviewDrawer.tsx b/front/components/assistant_builder/AssistantBuilderPreviewDrawer.tsx index 9bf4519e16de..614b62524b59 100644 --- a/front/components/assistant_builder/AssistantBuilderPreviewDrawer.tsx +++ b/front/components/assistant_builder/AssistantBuilderPreviewDrawer.tsx @@ -1,6 +1,8 @@ import { + BarChartIcon, Button, ChatBubbleBottomCenterTextIcon, + classNames, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -27,6 +29,7 @@ import { useContext, useEffect } from "react"; import ConversationViewer from "@app/components/assistant/conversation/ConversationViewer"; import { GenerationContextProvider } from "@app/components/assistant/conversation/GenerationContextProvider"; import { AssistantInputBar } from "@app/components/assistant/conversation/input_bar/InputBar"; +import { FeedbacksSection } from "@app/components/assistant_builder/AssistantBuilderPreviewDrawerFeedbacks"; import { usePreviewAssistant, useTryAssistantCore, @@ -41,7 +44,6 @@ import { getDefaultActionConfiguration } from "@app/components/assistant_builder import { ConfirmContext } from "@app/components/Confirm"; import { ACTION_SPECIFICATIONS } from "@app/lib/api/assistant/actions/utils"; import { useUser } from "@app/lib/swr/user"; -import { classNames } from "@app/lib/utils"; import type { FetchAssistantTemplateResponse } from "@app/pages/api/w/[wId]/assistant/builder/templates/[tId]"; interface AssistantBuilderRightPanelProps { @@ -54,6 +56,7 @@ interface AssistantBuilderRightPanelProps { rightPanelStatus: AssistantBuilderRightPanelStatus; openRightPanelTab: (tabName: AssistantBuilderRightPanelTab) => void; builderState: AssistantBuilderState; + agentConfigurationId: string | null; setAction: (action: AssistantBuilderSetActionType) => void; } @@ -67,6 +70,7 @@ export default function AssistantBuilderRightPanel({ rightPanelStatus, openRightPanelTab, builderState, + agentConfigurationId, setAction, }: AssistantBuilderRightPanelProps) { const { @@ -104,31 +108,39 @@ export default function AssistantBuilderRightPanel({ return ( - {template && ( - - - openRightPanelTab(t as AssistantBuilderRightPanelTab) - } - className="hidden lg:flex" - > - + + + openRightPanelTab(t as AssistantBuilderRightPanelTab) + } + className="hidden lg:flex" + > + + {template && ( + )} + {/* The agentConfigurationId is truthy iff not a new assistant */} + {agentConfigurationId && ( - - - - )} + )} + + + + 1000 ? "animate-reload" - : "" + : "", + rightPanelStatus.tab !== "Performance" ? "bg-structure-50" : "" )} > {(rightPanelStatus.tab === "Preview" || screen === "naming") && @@ -263,6 +276,15 @@ export default function AssistantBuilderRightPanel({ )} + {rightPanelStatus.tab === "Performance" && agentConfigurationId && ( + + + + + )} ); diff --git a/front/components/assistant_builder/AssistantBuilderPreviewDrawerFeedbacks.tsx b/front/components/assistant_builder/AssistantBuilderPreviewDrawerFeedbacks.tsx new file mode 100644 index 000000000000..1d6bfccc01ab --- /dev/null +++ b/front/components/assistant_builder/AssistantBuilderPreviewDrawerFeedbacks.tsx @@ -0,0 +1,244 @@ +import { + Avatar, + Button, + ExternalLinkIcon, + HandThumbDownIcon, + HandThumbUpIcon, + Page, + Spinner, +} from "@dust-tt/sparkle"; +import type { + LightAgentConfigurationType, + LightWorkspaceType, +} from "@dust-tt/types"; +import { memo, useCallback, useEffect } from "react"; +import { useInView } from "react-intersection-observer"; + +import type { AgentMessageFeedbackWithMetadataType } from "@app/lib/api/assistant/feedback"; +import { + useAgentConfigurationFeedbacksByDescVersion, + useAgentConfigurationHistory, +} from "@app/lib/swr/assistants"; +import { formatTimestampToFriendlyDate, timeAgoFrom } from "@app/lib/utils"; + +const FEEDBACKS_PAGE_SIZE = 50; + +interface FeedbacksSectionProps { + owner: LightWorkspaceType; + agentConfigurationId: string; +} +export const FeedbacksSection = ({ + owner, + agentConfigurationId, +}: FeedbacksSectionProps) => { + const { + isAgentConfigurationFeedbacksLoading, + isValidating, + agentConfigurationFeedbacks, + hasMore, + setSize, + size, + } = useAgentConfigurationFeedbacksByDescVersion({ + workspaceId: owner.sId, + agentConfigurationId: agentConfigurationId, + limit: FEEDBACKS_PAGE_SIZE, + }); + + // Intersection observer to detect when the user has scrolled to the bottom of the list. + const { ref: bottomRef, inView: isBottomOfListVisible } = useInView(); + useEffect(() => { + if ( + isBottomOfListVisible && + hasMore && + !isValidating && + !isAgentConfigurationFeedbacksLoading + ) { + void setSize(size + 1); + } + }, [ + isBottomOfListVisible, + hasMore, + isValidating, + isAgentConfigurationFeedbacksLoading, + setSize, + size, + ]); + + const { agentConfigurationHistory, isAgentConfigurationHistoryLoading } = + useAgentConfigurationHistory({ + workspaceId: owner.sId, + agentConfigurationId: agentConfigurationId, + disabled: !agentConfigurationId, + }); + + if ( + isAgentConfigurationFeedbacksLoading || + isAgentConfigurationHistoryLoading + ) { + return ; + } + + if ( + !isAgentConfigurationFeedbacksLoading && + (!agentConfigurationFeedbacks || agentConfigurationFeedbacks.length === 0) + ) { + return No feedbacks.; + } + + if (!agentConfigurationHistory) { + return ( + + Error loading the previous agent versions. + + ); + } + + return ( + + + + {agentConfigurationFeedbacks?.map((feedback, index) => { + const isFirstFeedback = index === 0; + const previousFeedbackHasDifferentVersion = + !isFirstFeedback && + feedback.agentConfigurationVersion !== + agentConfigurationFeedbacks[index - 1].agentConfigurationVersion; + return ( + + {previousFeedbackHasDifferentVersion && ( + c.version === feedback.agentConfigurationVersion + )} + agentConfigurationVersion={feedback.agentConfigurationVersion} + isLatestVersion={false} + /> + )} + {!previousFeedbackHasDifferentVersion && !isFirstFeedback && ( + + + + )} + + + + + ); + })} + + {/* Invisible div to act as a scroll anchor for detecting when the user has scrolled to the bottom */} + + + ); +}; + +interface AgentConfigurationVersionHeaderProps { + agentConfigurationVersion: number; + agentConfiguration: LightAgentConfigurationType | undefined; + isLatestVersion: boolean; +} +function AgentConfigurationVersionHeader({ + agentConfigurationVersion, + agentConfiguration, + isLatestVersion, +}: AgentConfigurationVersionHeaderProps) { + const getAgentConfigurationVersionString = useCallback( + (config: LightAgentConfigurationType) => { + if (isLatestVersion) { + return "Latest version"; + } + if (!config.versionCreatedAt) { + return `v${config.version}`; + } + const versionDate = new Date(config.versionCreatedAt); + return formatTimestampToFriendlyDate(versionDate.getTime(), "long"); + }, + [isLatestVersion] + ); + + return ( + + {agentConfiguration + ? getAgentConfigurationVersionString(agentConfiguration) + : `v${agentConfigurationVersion}`} + + ); +} + +interface FeedbackCardProps { + owner: LightWorkspaceType; + feedback: AgentMessageFeedbackWithMetadataType; +} +const MemoizedFeedbackCard = memo(FeedbackCard); +function FeedbackCard({ owner, feedback }: FeedbackCardProps) { + const conversationUrl = + feedback.conversationId && + feedback.messageId && + // IMPORTANT: We need to check if the conversation is shared before displaying it. + // This check is redundant: the conversationId is null if the conversation is not shared. + feedback.isConversationShared + ? `${process.env.NEXT_PUBLIC_DUST_CLIENT_FACING_URL}/w/${owner.sId}/assistant/${feedback.conversationId}?messageId=${feedback.messageId}` + : null; + + const timeSinceFeedback = timeAgoFrom( + new Date(feedback.createdAt).getTime(), + { + useLongFormat: true, + } + ); + + return ( + + + + {feedback.userImageUrl ? ( + + ) : ( + + )} + + {feedback.userName} + + + + + {timeSinceFeedback} ago + + {feedback.thumbDirection === "up" ? ( + + ) : ( + + )} + + + + + + {feedback.content && ( + + + {feedback.content} + + + )} + + ); +} diff --git a/front/components/assistant_builder/PrevNextButtons.tsx b/front/components/assistant_builder/PrevNextButtons.tsx index 9eb27518a9c8..79715a446303 100644 --- a/front/components/assistant_builder/PrevNextButtons.tsx +++ b/front/components/assistant_builder/PrevNextButtons.tsx @@ -20,7 +20,7 @@ export function PrevNextButtons({ { const newScreen = screen === "actions" ? "instructions" : "actions"; setScreen(newScreen); @@ -33,7 +33,7 @@ export function PrevNextButtons({ { const newScreen = screen === "instructions" ? "actions" : "naming"; setScreen(newScreen); diff --git a/front/lib/api/assistant/feedback.ts b/front/lib/api/assistant/feedback.ts index 00e4dcfd6b03..9686022ed1ee 100644 --- a/front/lib/api/assistant/feedback.ts +++ b/front/lib/api/assistant/feedback.ts @@ -209,11 +209,14 @@ export async function getAgentFeedbacks({ return new Err(new Error("agent_configuration_not_found")); } - const feedbacksRes = await AgentMessageFeedbackResource.fetch({ - workspace: owner, - agentConfiguration, - paginationParams, - }); + const feedbacksRes = + await AgentMessageFeedbackResource.getAgentConfigurationFeedbacksByDescVersion( + { + workspace: owner, + agentConfiguration, + paginationParams, + } + ); const feedbacks = feedbacksRes.map((feedback) => feedback.toJSON()); @@ -223,7 +226,7 @@ export async function getAgentFeedbacks({ const feedbacksWithHiddenConversationId = feedbacks.map((feedback) => ({ ...feedback, - // Only display conversationId if the feedback was shared + // Redact the conversationId if user did not share the conversation. conversationId: feedback.isConversationShared ? feedback.conversationId : null, diff --git a/front/lib/resources/agent_message_feedback_resource.ts b/front/lib/resources/agent_message_feedback_resource.ts index 7e1e5dbf0197..e8bb69e48754 100644 --- a/front/lib/resources/agent_message_feedback_resource.ts +++ b/front/lib/resources/agent_message_feedback_resource.ts @@ -120,18 +120,19 @@ export class AgentMessageFeedbackResource extends BaseResource = { - // IMPORTANT: Necessary for global models who share ids across workspaces. + // Safety check: global models share ids across workspaces and some have had feedbacks. workspaceId: workspace.id, + agentConfigurationId: agentConfiguration.sId, }; if (paginationParams.lastValue) { @@ -140,9 +141,6 @@ export class AgentMessageFeedbackResource extends BaseResource = fetcher; + + const urlParams = new URLSearchParams({ + limit: limit.toString(), + orderColumn: "id", + orderDirection: "desc", + withMetadata: "true", + }); + + const [hasMore, setHasMore] = useState(true); + + const { data, error, mutate, size, setSize, isLoading, isValidating } = + useSWRInfiniteWithDefaults( + (pageIndex: number, previousPageData) => { + if (!agentConfigurationId) { + return null; + } + + // If we have reached the last page and there are no more + // messages or the previous page has no messages, return null. + if (previousPageData && previousPageData.feedbacks.length < limit) { + setHasMore(false); + return null; + } + + if (previousPageData !== null) { + const lastIdValue = + previousPageData.feedbacks[previousPageData.feedbacks.length - 1] + .id; + urlParams.append("lastValue", lastIdValue.toString()); + } + return `/api/w/${workspaceId}/assistant/agent_configurations/${agentConfigurationId}/feedbacks?${urlParams.toString()}`; + }, + agentConfigurationFeedbacksFetcher, + { + revalidateAll: false, + revalidateOnFocus: false, + } + ); + + return { + isLoadingInitialData: !error && !data, + isAgentConfigurationFeedbacksError: error, + isAgentConfigurationFeedbacksLoading: isLoading, + isValidating, + agentConfigurationFeedbacks: useMemo( + () => (data ? data.flatMap((d) => (d ? d.feedbacks : [])) : []), + [data] + ), + hasMore, + mutateAgentConfigurationFeedbacks: mutate, + setSize, + size, + }; +} + export function useAgentConfigurationHistory({ workspaceId, agentConfigurationId, diff --git a/front/pages/api/w/[wId]/assistant/agent_configurations/[aId]/feedbacks.ts b/front/pages/api/w/[wId]/assistant/agent_configurations/[aId]/feedbacks.ts index 332f812296ee..ee5315167db2 100644 --- a/front/pages/api/w/[wId]/assistant/agent_configurations/[aId]/feedbacks.ts +++ b/front/pages/api/w/[wId]/assistant/agent_configurations/[aId]/feedbacks.ts @@ -13,7 +13,9 @@ import { apiError } from "@app/logger/withlogging"; async function handler( req: NextApiRequest, res: NextApiResponse< - WithAPIErrorResponse<{ feedbacks: AgentMessageFeedbackType[] }> + WithAPIErrorResponse<{ + feedbacks: AgentMessageFeedbackType[]; + }> >, auth: Authenticator ): Promise { @@ -77,7 +79,9 @@ async function handler( const feedbacks = feedbacksRes.value; - res.status(200).json({ feedbacks }); + res.status(200).json({ + feedbacks: feedbacks, + }); return; default: diff --git a/types/src/front/assistant/builder.ts b/types/src/front/assistant/builder.ts index 29eec535d415..ce14c312294e 100644 --- a/types/src/front/assistant/builder.ts +++ b/types/src/front/assistant/builder.ts @@ -28,7 +28,11 @@ export const ASSISTANT_CREATIVITY_LEVEL_TEMPERATURES: Record< creative: 1.0, }; -export const ASSISTANT_BUILDER_DRAWER_TABS = ["Template", "Preview"] as const; +export const ASSISTANT_BUILDER_DRAWER_TABS = [ + "Template", + "Preview", + "Performance", +] as const; export type AssistantBuilderRightPanelTab = (typeof ASSISTANT_BUILDER_DRAWER_TABS)[number];