From 919acc745165ed9c715e90e21f6f0315403dfd6b Mon Sep 17 00:00:00 2001 From: Brian Newsom Date: Wed, 27 May 2026 14:51:36 -0600 Subject: [PATCH 1/3] feat(nemo-studio): Move intake to span based model Signed-off-by: Brian Newsom --- web/packages/common/src/constants/query.ts | 7 +- web/packages/common/src/utils/chat.ts | 68 --- web/packages/common/src/utils/query.ts | 12 +- .../studio/src/api/intake/constants.ts | 15 - web/packages/studio/src/api/intake/utils.ts | 68 --- .../components/DatasetFileSelect/constants.ts | 7 +- .../components/DatasetFileSelect/index.tsx | 6 +- .../IntakeAnnotationPanel/index.tsx | 90 --- .../IntakeAnnotationsPanel/index.spec.tsx | 55 ++ .../IntakeAnnotationsPanel/index.tsx | 302 ++++++++++ .../EntryBulkDeleteModal.spec.tsx | 127 ---- .../EntryBulkDeleteModal.tsx | 99 --- .../EntryBulkExportModal.spec.tsx | 148 ----- .../EntryBulkExportModal.tsx | 279 --------- .../IntakeEntriesTable/EntryRatingCell.tsx | 25 - .../IntakeEntriesTable/EntryStatusCell.tsx | 31 - .../IntakeEntriesTable/EntryThumbCell.tsx | 40 -- .../IntakeEntriesTable.stories.tsx | 65 -- .../IntakeEntriesTable/index.spec.tsx | 93 --- .../components/IntakeEntriesTable/index.tsx | 381 ------------ .../IntakeEntriesTable/utils.spec.ts | 133 ---- .../components/IntakeEntriesTable/utils.ts | 89 --- .../IntakeEntryConversation/README.md | 67 -- .../components/Annotation.test.tsx | 144 ----- .../components/Annotation.tsx | 46 -- .../components/AssistantResponse.test.tsx | 128 ---- .../components/AssistantResponse.tsx | 57 -- .../components/ChatView.tsx | 58 -- .../components/JSONView.tsx | 29 - .../components/LastUserMessage.tsx | 42 -- .../components/SystemPrompt.test.tsx | 87 --- .../components/SystemPrompt.tsx | 66 -- .../components/ThumbStatus.test.tsx | 105 ---- .../components/ThumbStatus.tsx | 62 -- .../IntakeEntryConversation/index.spec.tsx | 72 --- .../IntakeEntryConversation/index.tsx | 100 --- .../src/components/IntakeEventsList/README.md | 21 - .../components/EventListItem.test.tsx | 223 ------- .../components/EventListItem.tsx | 40 -- .../components/EventListItemLayout.test.tsx | 170 ------ .../components/EventListItemLayout.tsx | 104 ---- .../ReviewerAnnotationEvent.test.tsx | 243 -------- .../components/ReviewerAnnotationEvent.tsx | 114 ---- .../components/ThumbTag.test.tsx | 33 - .../IntakeEventsList/components/ThumbTag.tsx | 27 - .../components/UserActionEvent.test.tsx | 150 ----- .../components/UserActionEvent.tsx | 63 -- .../components/UserFeedbackEvent.test.tsx | 209 ------- .../components/UserFeedbackEvent.tsx | 97 --- .../src/components/IntakeEventsList/index.tsx | 46 -- .../IntakeSpansTable/index.spec.tsx | 26 + .../src/components/IntakeSpansTable/index.tsx | 351 +++++++++++ .../IntakeTelemetryDataView/index.tsx | 280 +++++++++ .../IntakeTelemetryStatusBadge/index.spec.tsx | 19 + .../IntakeTelemetryStatusBadge/index.tsx | 52 ++ .../components/AssistantMessageBubble.tsx | 130 ---- .../components/SystemMessageBubble.tsx | 45 -- .../components/ThreadConversation.tsx | 104 ---- .../components/ToolResponseBubble.tsx | 37 -- .../components/UserMessageBubble.tsx | 31 - .../components/IntakeThreadPanel/index.tsx | 63 -- .../IntakeThreadsTable.stories.tsx | 188 ------ .../components/IntakeThreadsTable/index.tsx | 317 ---------- .../IntakeTracesTable/index.spec.tsx | 25 + .../components/IntakeTracesTable/index.tsx | 209 +++++++ .../ExportEntriesButton/index.spec.tsx | 123 ---- .../buttons/ExportEntriesButton/index.tsx | 100 --- .../FeedbackRatingPieChart/constants.ts | 5 - .../charts/FeedbackRatingPieChart/index.tsx | 84 --- .../FeedbackSentimentLineChart/index.tsx | 106 ---- .../FeedbackSentimentLineChart/utils.spec.ts | 76 --- .../FeedbackSentimentLineChart/utils.ts | 42 -- .../dataViews/CustomModelsDataView/index.tsx | 6 +- .../ExportJobsDataView.stories.tsx | 53 -- .../ExportJobsDataView/index.spec.tsx | 175 ------ .../dataViews/ExportJobsDataView/index.tsx | 167 ----- .../AnnotationForm/AnnotationErrorMessage.tsx | 20 - .../form/AnnotationForm/constants.ts | 24 - .../components/form/AnnotationForm/index.tsx | 120 ---- .../useCreateReviewerAnnotation.tsx | 36 -- .../form/AnnotationForm/utils.spec.ts | 42 -- .../components/form/AnnotationForm/utils.ts | 37 -- .../form/NewExportJobForm/constants.tsx | 153 ----- .../form/NewExportJobForm/index.spec.tsx | 268 -------- .../form/NewExportJobForm/index.tsx | 254 -------- .../modals/AnnotationModal/index.tsx | 88 --- .../sidePanels/ExportJobPanel/index.tsx | 76 --- .../studio/src/constants/intakeJobs.ts | 12 - web/packages/studio/src/constants/routes.ts | 14 +- web/packages/studio/src/mocks/handlers.ts | 77 ++- .../studio/src/mocks/intake/entries.ts | 122 ---- .../studio/src/mocks/intake/exportJobs.ts | 48 -- .../studio/src/mocks/intake/telemetry.ts | 292 +++++++++ .../CustomizationJobListRoute/index.tsx | 6 +- .../src/routes/IntakeEntriesRoute/index.tsx | 24 - .../routes/IntakeEntryEventsRoute/index.tsx | 88 --- .../src/routes/IntakeEntryLayout/index.tsx | 135 ----- .../routes/IntakeEntryMessagesRoute/index.tsx | 64 -- .../routes/IntakeEntryMetadataRoute/index.tsx | 237 -------- .../routes/IntakeExportJobsRoute/index.tsx | 9 - .../studio/src/routes/IntakeLayout/index.tsx | 48 +- .../routes/IntakeSpanDetailRoute/index.tsx | 331 ++++++++++ .../src/routes/IntakeThreadsRoute/index.tsx | 23 - .../IntakeTraceDetailRoute/index.spec.tsx | 36 ++ .../routes/IntakeTraceDetailRoute/index.tsx | 265 ++++++++ .../WorkspaceLayout/WorkspaceSideNav.tsx | 4 +- web/packages/studio/src/routes/index.spec.tsx | 11 +- web/packages/studio/src/routes/index.tsx | 100 ++- web/packages/studio/src/routes/utils.ts | 45 +- .../studio/src/tests/title-change.spec.tsx | 3 +- web/packages/studio/src/util/entries.spec.ts | 570 ------------------ web/packages/studio/src/util/entries.ts | 251 -------- .../studio/src/util/intakeTelemetry.spec.ts | 117 ++++ .../studio/src/util/intakeTelemetry.ts | 158 +++++ 114 files changed, 2690 insertions(+), 9075 deletions(-) delete mode 100644 web/packages/studio/src/api/intake/constants.ts delete mode 100644 web/packages/studio/src/api/intake/utils.ts delete mode 100644 web/packages/studio/src/components/IntakeAnnotationPanel/index.tsx create mode 100644 web/packages/studio/src/components/IntakeAnnotationsPanel/index.spec.tsx create mode 100644 web/packages/studio/src/components/IntakeAnnotationsPanel/index.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntriesTable/EntryBulkDeleteModal.spec.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntriesTable/EntryBulkDeleteModal.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntriesTable/EntryBulkExportModal.spec.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntriesTable/EntryBulkExportModal.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntriesTable/EntryRatingCell.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntriesTable/EntryStatusCell.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntriesTable/EntryThumbCell.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntriesTable/IntakeEntriesTable.stories.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntriesTable/index.spec.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntriesTable/index.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntriesTable/utils.spec.ts delete mode 100644 web/packages/studio/src/components/IntakeEntriesTable/utils.ts delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/README.md delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/components/Annotation.test.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/components/Annotation.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/components/AssistantResponse.test.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/components/AssistantResponse.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/components/ChatView.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/components/JSONView.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/components/LastUserMessage.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/components/SystemPrompt.test.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/components/SystemPrompt.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/components/ThumbStatus.test.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/components/ThumbStatus.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/index.spec.tsx delete mode 100644 web/packages/studio/src/components/IntakeEntryConversation/index.tsx delete mode 100644 web/packages/studio/src/components/IntakeEventsList/README.md delete mode 100644 web/packages/studio/src/components/IntakeEventsList/components/EventListItem.test.tsx delete mode 100644 web/packages/studio/src/components/IntakeEventsList/components/EventListItem.tsx delete mode 100644 web/packages/studio/src/components/IntakeEventsList/components/EventListItemLayout.test.tsx delete mode 100644 web/packages/studio/src/components/IntakeEventsList/components/EventListItemLayout.tsx delete mode 100644 web/packages/studio/src/components/IntakeEventsList/components/ReviewerAnnotationEvent.test.tsx delete mode 100644 web/packages/studio/src/components/IntakeEventsList/components/ReviewerAnnotationEvent.tsx delete mode 100644 web/packages/studio/src/components/IntakeEventsList/components/ThumbTag.test.tsx delete mode 100644 web/packages/studio/src/components/IntakeEventsList/components/ThumbTag.tsx delete mode 100644 web/packages/studio/src/components/IntakeEventsList/components/UserActionEvent.test.tsx delete mode 100644 web/packages/studio/src/components/IntakeEventsList/components/UserActionEvent.tsx delete mode 100644 web/packages/studio/src/components/IntakeEventsList/components/UserFeedbackEvent.test.tsx delete mode 100644 web/packages/studio/src/components/IntakeEventsList/components/UserFeedbackEvent.tsx delete mode 100644 web/packages/studio/src/components/IntakeEventsList/index.tsx create mode 100644 web/packages/studio/src/components/IntakeSpansTable/index.spec.tsx create mode 100644 web/packages/studio/src/components/IntakeSpansTable/index.tsx create mode 100644 web/packages/studio/src/components/IntakeTelemetryDataView/index.tsx create mode 100644 web/packages/studio/src/components/IntakeTelemetryStatusBadge/index.spec.tsx create mode 100644 web/packages/studio/src/components/IntakeTelemetryStatusBadge/index.tsx delete mode 100644 web/packages/studio/src/components/IntakeThreadPanel/components/AssistantMessageBubble.tsx delete mode 100644 web/packages/studio/src/components/IntakeThreadPanel/components/SystemMessageBubble.tsx delete mode 100644 web/packages/studio/src/components/IntakeThreadPanel/components/ThreadConversation.tsx delete mode 100644 web/packages/studio/src/components/IntakeThreadPanel/components/ToolResponseBubble.tsx delete mode 100644 web/packages/studio/src/components/IntakeThreadPanel/components/UserMessageBubble.tsx delete mode 100644 web/packages/studio/src/components/IntakeThreadPanel/index.tsx delete mode 100644 web/packages/studio/src/components/IntakeThreadsTable/IntakeThreadsTable.stories.tsx delete mode 100644 web/packages/studio/src/components/IntakeThreadsTable/index.tsx create mode 100644 web/packages/studio/src/components/IntakeTracesTable/index.spec.tsx create mode 100644 web/packages/studio/src/components/IntakeTracesTable/index.tsx delete mode 100644 web/packages/studio/src/components/buttons/ExportEntriesButton/index.spec.tsx delete mode 100644 web/packages/studio/src/components/buttons/ExportEntriesButton/index.tsx delete mode 100644 web/packages/studio/src/components/charts/FeedbackRatingPieChart/constants.ts delete mode 100644 web/packages/studio/src/components/charts/FeedbackRatingPieChart/index.tsx delete mode 100644 web/packages/studio/src/components/charts/FeedbackSentimentLineChart/index.tsx delete mode 100644 web/packages/studio/src/components/charts/FeedbackSentimentLineChart/utils.spec.ts delete mode 100644 web/packages/studio/src/components/charts/FeedbackSentimentLineChart/utils.ts delete mode 100644 web/packages/studio/src/components/dataViews/ExportJobsDataView/ExportJobsDataView.stories.tsx delete mode 100644 web/packages/studio/src/components/dataViews/ExportJobsDataView/index.spec.tsx delete mode 100644 web/packages/studio/src/components/dataViews/ExportJobsDataView/index.tsx delete mode 100644 web/packages/studio/src/components/form/AnnotationForm/AnnotationErrorMessage.tsx delete mode 100644 web/packages/studio/src/components/form/AnnotationForm/constants.ts delete mode 100644 web/packages/studio/src/components/form/AnnotationForm/index.tsx delete mode 100644 web/packages/studio/src/components/form/AnnotationForm/useCreateReviewerAnnotation.tsx delete mode 100644 web/packages/studio/src/components/form/AnnotationForm/utils.spec.ts delete mode 100644 web/packages/studio/src/components/form/AnnotationForm/utils.ts delete mode 100644 web/packages/studio/src/components/form/NewExportJobForm/constants.tsx delete mode 100644 web/packages/studio/src/components/form/NewExportJobForm/index.spec.tsx delete mode 100644 web/packages/studio/src/components/form/NewExportJobForm/index.tsx delete mode 100644 web/packages/studio/src/components/modals/AnnotationModal/index.tsx delete mode 100644 web/packages/studio/src/components/sidePanels/ExportJobPanel/index.tsx delete mode 100644 web/packages/studio/src/constants/intakeJobs.ts delete mode 100644 web/packages/studio/src/mocks/intake/entries.ts delete mode 100644 web/packages/studio/src/mocks/intake/exportJobs.ts create mode 100644 web/packages/studio/src/mocks/intake/telemetry.ts delete mode 100644 web/packages/studio/src/routes/IntakeEntriesRoute/index.tsx delete mode 100644 web/packages/studio/src/routes/IntakeEntryEventsRoute/index.tsx delete mode 100644 web/packages/studio/src/routes/IntakeEntryLayout/index.tsx delete mode 100644 web/packages/studio/src/routes/IntakeEntryMessagesRoute/index.tsx delete mode 100644 web/packages/studio/src/routes/IntakeEntryMetadataRoute/index.tsx delete mode 100644 web/packages/studio/src/routes/IntakeExportJobsRoute/index.tsx create mode 100644 web/packages/studio/src/routes/IntakeSpanDetailRoute/index.tsx delete mode 100644 web/packages/studio/src/routes/IntakeThreadsRoute/index.tsx create mode 100644 web/packages/studio/src/routes/IntakeTraceDetailRoute/index.spec.tsx create mode 100644 web/packages/studio/src/routes/IntakeTraceDetailRoute/index.tsx delete mode 100644 web/packages/studio/src/util/entries.spec.ts delete mode 100644 web/packages/studio/src/util/entries.ts create mode 100644 web/packages/studio/src/util/intakeTelemetry.spec.ts create mode 100644 web/packages/studio/src/util/intakeTelemetry.ts diff --git a/web/packages/common/src/constants/query.ts b/web/packages/common/src/constants/query.ts index 8fd8a57f1d..4ea46440b5 100644 --- a/web/packages/common/src/constants/query.ts +++ b/web/packages/common/src/constants/query.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { JobStatus as IJobStatus, PlatformJobStatus } from '@nemo/sdk/generated/platform/schema'; +import { PlatformJobStatus } from '@nemo/sdk/generated/platform/schema'; // Customizer uses Platform SDK status export const CJobCancellableStatuses: PlatformJobStatus[] = [ @@ -16,11 +16,6 @@ export const CJobTerminalStatuses: PlatformJobStatus[] = [ PlatformJobStatus.error, // was 'failed' PlatformJobStatus.cancelled, ]; -export const IJobTerminalStatuses: IJobStatus[] = [ - IJobStatus.completed, - IJobStatus.failed, - IJobStatus.cancelled, -]; export const PlatformJobTerminalStatuses: PlatformJobStatus[] = [ PlatformJobStatus.completed, PlatformJobStatus.cancelled, diff --git a/web/packages/common/src/utils/chat.ts b/web/packages/common/src/utils/chat.ts index 0d75bed77a..35c95043b3 100644 --- a/web/packages/common/src/utils/chat.ts +++ b/web/packages/common/src/utils/chat.ts @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { FlexibleMessage } from '@nemo/sdk/generated/platform/schema'; import { ChatCompletion, ChatCompletionChunk, @@ -71,70 +70,3 @@ export const maybeInsertSystemMessage = ( ...parsedMessages, ]; }; - -/** - * Safely extracts string content from a FlexibleMessage. - * Returns empty string if content is undefined, null, or not a string. - */ -const getStringContent = (content: unknown): string => { - if (typeof content === 'string') return content; - if (content === null || content === undefined) return ''; - // Handle array content (OpenAI multi-part messages) by joining text parts - if (Array.isArray(content)) { - return content - .filter( - (part): part is { type: 'text'; text: string } => - typeof part === 'object' && part?.type === 'text' && typeof part?.text === 'string' - ) - .map((part) => part.text) - .join(' '); - } - return ''; -}; - -/** - * Converts a FlexibleMessage (intake API) to ChatCompletionMessageParam (OpenAI). - * FlexibleMessage is provider-agnostic; this maps it to the OpenAI standard. - */ -export const toOpenAIMessage = (message: FlexibleMessage): ChatCompletionMessageParam => { - const { role, content, name, tool_calls, tool_call_id } = message; - const stringContent = getStringContent(content); - - switch (role) { - case 'user': - return { - role: 'user', - content: stringContent, - ...(typeof name === 'string' && { name }), - }; - case 'assistant': - return { - role: 'assistant', - content: stringContent || null, - ...(Array.isArray(tool_calls) && { tool_calls }), - }; - case 'system': - return { - role: 'system', - content: stringContent, - ...(typeof name === 'string' && { name }), - }; - case 'tool': - return { - role: 'tool', - content: stringContent, - tool_call_id: typeof tool_call_id === 'string' ? tool_call_id : '', - }; - case 'function': - // Map legacy 'function' role to 'tool' for OpenAI compatibility - return { - role: 'tool', - content: stringContent, - tool_call_id: typeof tool_call_id === 'string' ? tool_call_id : '', - }; - case 'developer': - return { role: 'developer', content: stringContent }; - default: - return { role: 'user', content: stringContent }; - } -}; diff --git a/web/packages/common/src/utils/query.ts b/web/packages/common/src/utils/query.ts index d5d57e8759..9d9b13c356 100644 --- a/web/packages/common/src/utils/query.ts +++ b/web/packages/common/src/utils/query.ts @@ -1,18 +1,14 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { JobStatus as IJobStatus, PlatformJobStatus } from '@nemo/sdk/generated/platform/schema'; +import { PlatformJobStatus } from '@nemo/sdk/generated/platform/schema'; import * as DataView from '../components/DataView/internal'; import { JOB_POLLING_INTERVAL_MS } from '../constants'; -import { CJobTerminalStatuses, IJobTerminalStatuses } from '../constants/query'; +import { CJobTerminalStatuses } from '../constants/query'; -export const getJobRefetchInterval = (status?: PlatformJobStatus | IJobStatus) => { - if ( - !status || - (!CJobTerminalStatuses.includes(status as PlatformJobStatus) && - !IJobTerminalStatuses.includes(status as IJobStatus)) - ) { +export const getJobRefetchInterval = (status?: PlatformJobStatus) => { + if (!status || !CJobTerminalStatuses.includes(status)) { return JOB_POLLING_INTERVAL_MS; } return false; diff --git a/web/packages/studio/src/api/intake/constants.ts b/web/packages/studio/src/api/intake/constants.ts deleted file mode 100644 index 9498b1990e..0000000000 --- a/web/packages/studio/src/api/intake/constants.ts +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -export const FEEDBACK_CATEGORY_KEY = 'studio_feedback'; -export const FEEDBACK_CATEGORIES = [ - 'Too much information', - 'Not enough information', - 'Not factually correct', - "Didn't fully follow instructions", -]; - -export enum FeedbackAddToDatasetFileSource { - New = 'Create new', - Existing = 'Add to existing', -} diff --git a/web/packages/studio/src/api/intake/utils.ts b/web/packages/studio/src/api/intake/utils.ts deleted file mode 100644 index 31eecfdd29..0000000000 --- a/web/packages/studio/src/api/intake/utils.ts +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import type { EntryFilter } from '@nemo/sdk/generated/platform/schema'; -import { QUERY_PARAMETERS } from '@studio/routes/constants'; -import { ChatCompletionMessageParam } from 'openai/resources/index.mjs'; - -/** - * Recursively processes a filter object and adds its properties to URLSearchParams. - * Handles nested objects, arrays with operators (like 'in'), and primitive values. - * - * @param obj - The object to process - * @param params - The URLSearchParams instance to populate - * @param prefix - The current key prefix for nested properties (e.g., 'context', 'created_at') - */ -const processFilterObject = (obj: EntryFilter, params: URLSearchParams, prefix = ''): void => { - for (const [key, value] of Object.entries(obj)) { - // Skip undefined and null values - if (value === undefined || value === null) { - continue; - } - - // Build the parameter key (e.g., 'context.app', 'created_at.gte') - const paramKey = prefix ? `${prefix}.${key}` : key; - - // Handle array values with operators (e.g., {in: ['id1', 'id2']}) - if (Array.isArray(value)) { - value.forEach((item) => { - if (item !== undefined && item !== null) { - params.append(paramKey, String(item)); - } - }); - } - // Handle nested objects recursively - else if (typeof value === 'object' && value !== null) { - processFilterObject(value, params, paramKey); - } - // Handle primitive values (string, number, boolean) - else { - params.set(paramKey, String(value)); - } - } -}; - -export const generateFilterParam = (filter?: EntryFilter): string => { - if (!filter) { - return ''; - } - const params = new URLSearchParams(); - - // Special handling for 'project' field to use QUERY_PARAMETERS constant - if (filter.project && typeof filter.project === 'string') { - params.set(QUERY_PARAMETERS.project, filter.project); - const withoutProject = { ...filter, project: undefined }; - processFilterObject(withoutProject, params); - } else { - processFilterObject(filter, params); - } - - return params.toString(); -}; - -export const isToolCallMessage = (message: ChatCompletionMessageParam) => { - return ( - message.role === 'tool' || - ('tool_calls' in message && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) - ); -}; diff --git a/web/packages/studio/src/components/DatasetFileSelect/constants.ts b/web/packages/studio/src/components/DatasetFileSelect/constants.ts index 2edfa8ec15..d646599b16 100644 --- a/web/packages/studio/src/components/DatasetFileSelect/constants.ts +++ b/web/packages/studio/src/components/DatasetFileSelect/constants.ts @@ -1,10 +1,15 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { MultiselectOption } from '@studio/constants/mutliselect'; +import type { MultiselectOption } from '@studio/constants/mutliselect'; export const LOADING_FILES_OPTION: MultiselectOption = { label: 'Loading files...', value: 'loading', isDisabled: true, }; + +export enum FeedbackAddToDatasetFileSource { + New = 'Create new', + Existing = 'Add to existing', +} diff --git a/web/packages/studio/src/components/DatasetFileSelect/index.tsx b/web/packages/studio/src/components/DatasetFileSelect/index.tsx index 643eaa3e5d..0ed036ab0b 100644 --- a/web/packages/studio/src/components/DatasetFileSelect/index.tsx +++ b/web/packages/studio/src/components/DatasetFileSelect/index.tsx @@ -15,8 +15,10 @@ import { SelectRoot, SelectTrigger, } from '@nvidia/foundations-react-core'; -import { FeedbackAddToDatasetFileSource } from '@studio/api/intake/constants'; -import { LOADING_FILES_OPTION } from '@studio/components/DatasetFileSelect/constants'; +import { + FeedbackAddToDatasetFileSource, + LOADING_FILES_OPTION, +} from '@studio/components/DatasetFileSelect/constants'; import { MultiselectOption } from '@studio/constants/mutliselect'; import { Plus } from 'lucide-react'; import { FC, ReactNode, useMemo } from 'react'; diff --git a/web/packages/studio/src/components/IntakeAnnotationPanel/index.tsx b/web/packages/studio/src/components/IntakeAnnotationPanel/index.tsx deleted file mode 100644 index 19383f7d84..0000000000 --- a/web/packages/studio/src/components/IntakeAnnotationPanel/index.tsx +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { zodResolver } from '@hookform/resolvers/zod'; -import { LoadingButton } from '@nemo/common/src/components/LoadingButton'; -import { useToast } from '@nemo/common/src/providers/toast/useToast'; -import { Entry } from '@nemo/sdk/generated/platform/schema'; -import { Flex, Panel, Stack } from '@nvidia/foundations-react-core'; -import { AnnotationForm } from '@studio/components/form/AnnotationForm'; -import { AnnotationErrorMessage } from '@studio/components/form/AnnotationForm/AnnotationErrorMessage'; -import { annotationFormFields } from '@studio/components/form/AnnotationForm/constants'; -import { useCreateReviewerAnnotation } from '@studio/components/form/AnnotationForm/useCreateReviewerAnnotation'; -import { formToReviewerAnnotationEvent } from '@studio/components/form/AnnotationForm/utils'; -import { getEntryResponseContent } from '@studio/components/IntakeEntriesTable/utils'; -import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; -import { handleFormErrorsGeneric } from '@studio/util/forms/error'; -import { ComponentProps, FC } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; -import { z } from 'zod'; - -interface Props { - entry: Entry; - attributes?: { - Panel?: ComponentProps; - }; -} - -export const IntakeAnnotationPanel: FC = ({ entry, attributes }) => { - const toast = useToast(); - const workspace = useWorkspaceFromPath(); - const form = useForm>({ - resolver: zodResolver(annotationFormFields), - mode: 'onChange', - values: { - modelResponse: getEntryResponseContent(entry), - }, - }); - - const { mutateAsync: createAnnotation, isPending: isCreatingAnnotation } = - useCreateReviewerAnnotation({ workspace, entryId: entry.id }); - const onSubmit = async (data: z.infer) => { - if (!entry.id) { - toast.error('Cannot create annotation without an existing entry.'); - return; - } - await createAnnotation({ - workspace, - name: entry.id, - data: { events: [formToReviewerAnnotationEvent(data)] }, - }); - }; - - const errorMessage = form.formState.errors.hasChanges?.message ? ( - - ) : undefined; - const flexJustify = errorMessage ? 'between' : 'end'; - - return ( - - - -
- - {errorMessage} - - Submit - - - } - /> - -
-
-
- ); -}; diff --git a/web/packages/studio/src/components/IntakeAnnotationsPanel/index.spec.tsx b/web/packages/studio/src/components/IntakeAnnotationsPanel/index.spec.tsx new file mode 100644 index 0000000000..8d7743f89b --- /dev/null +++ b/web/packages/studio/src/components/IntakeAnnotationsPanel/index.spec.tsx @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { IntakeAnnotationsPanel } from '@studio/components/IntakeAnnotationsPanel'; +import { resetMockAnnotations } from '@studio/mocks/intake/telemetry'; +import { renderRoute, screen, waitFor, within } from '@studio/tests/util/render'; +import userEvent from '@testing-library/user-event'; + +const spanId = 'span-root-001'; +const sessionId = 'session-agent-run-001'; + +describe('IntakeAnnotationsPanel', () => { + beforeEach(() => { + resetMockAnnotations(); + }); + + it('lists and creates span annotations through the generated client', async () => { + const user = userEvent.setup(); + + renderRoute( + , + { history: '/workspaces/default/intake/spans/span-root-001' } + ); + + expect( + await screen.findByText('Good final response, but verify policy citations.') + ).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: /Negative/i })); + expect(await screen.findByText('Negative feedback')).toBeInTheDocument(); + + await user.type(screen.getByPlaceholderText('Add a note about this span.'), 'Needs review.'); + await user.click(screen.getByRole('button', { name: /Add Note/i })); + + expect(await screen.findByText('Needs review.')).toBeInTheDocument(); + }); + + it('deletes span annotations through the generated client', async () => { + const user = userEvent.setup(); + + renderRoute( + , + { history: '/workspaces/default/intake/spans/span-root-001' } + ); + + const note = await screen.findByRole('article', { name: 'Note annotation' }); + await user.click(within(note).getByRole('button', { name: /Delete/i })); + + await waitFor(() => { + expect( + screen.queryByText('Good final response, but verify policy citations.') + ).not.toBeInTheDocument(); + }); + }); +}); diff --git a/web/packages/studio/src/components/IntakeAnnotationsPanel/index.tsx b/web/packages/studio/src/components/IntakeAnnotationsPanel/index.tsx new file mode 100644 index 0000000000..3d84ed31aa --- /dev/null +++ b/web/packages/studio/src/components/IntakeAnnotationsPanel/index.tsx @@ -0,0 +1,302 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { formatAbsoluteTimestamp } from '@nemo/common/src/components/RelativeTime/util'; +import { + getListAnnotationsQueryKey, + useCreateAnnotation, + useDeleteAnnotation, + useListAnnotations, +} from '@nemo/sdk/generated/platform/api'; +import { + AnnotationSortField, + FeedbackAnnotationInputKind, + FeedbackAnnotationInputValue, + NoteAnnotationInputKind, + type Annotation, + type FeedbackAnnotationInputValue as FeedbackAnnotationInputValueType, +} from '@nemo/sdk/generated/platform/schema'; +import { + Button, + CodeSnippet, + Flex, + FormField, + Panel, + Stack, + Text, + TextArea, +} from '@nvidia/foundations-react-core'; +import { getErrorMessage } from '@studio/api/common/utils'; +import { ThumbButton } from '@studio/components/buttons/ThumbButton'; +import { useQueryClient } from '@tanstack/react-query'; +import { MessageSquarePlus, NotebookPen, Trash2 } from 'lucide-react'; +import { ChangeEvent, FC, FormEvent, useMemo, useState } from 'react'; + +const getAnnotationErrorMessage = (error: unknown, fallback: string): string => + error instanceof Error ? getErrorMessage(error, fallback) : fallback; + +const formatAnnotationTitle = (annotation: Annotation): string => { + switch (annotation.kind) { + case 'feedback': + return annotation.value === 'positive' ? 'Positive feedback' : 'Negative feedback'; + case 'note': + return 'Note'; + case 'label': + return annotation.name ? `Label: ${annotation.name}` : 'Label'; + case 'metadata': + return 'Metadata'; + } +}; + +const renderAnnotationBody = (annotation: Annotation) => { + switch (annotation.kind) { + case 'feedback': + return ( + + {annotation.value} + + ); + case 'note': + return ( + + {annotation.text} + + ); + case 'label': + return ( + + {String(annotation.value)} + + ); + case 'metadata': + return ( + + ); + } +}; + +export interface IntakeAnnotationsPanelProps { + workspace: string; + spanId: string; + sessionId: string; +} + +export const IntakeAnnotationsPanel: FC = ({ + workspace, + spanId, + sessionId, +}) => { + const queryClient = useQueryClient(); + const [noteText, setNoteText] = useState(''); + const [mutationError, setMutationError] = useState(); + + const listParams = useMemo( + () => ({ + page: 1, + page_size: 100, + sort: AnnotationSortField['-created_at'], + filter: { + span_id: spanId, + }, + }), + [spanId] + ); + + const { + data: annotationsResponse, + error: listError, + isLoading, + } = useListAnnotations(workspace, listParams); + const createAnnotation = useCreateAnnotation(); + const deleteAnnotation = useDeleteAnnotation(); + + const annotations = annotationsResponse?.data ?? []; + const activeFeedback = annotations.find((annotation) => annotation.kind === 'feedback'); + const isMutating = createAnnotation.isPending || deleteAnnotation.isPending; + + const refreshAnnotations = async (): Promise => { + await queryClient.invalidateQueries({ + queryKey: getListAnnotationsQueryKey(workspace), + }); + }; + + const handleFeedback = async (value: FeedbackAnnotationInputValueType): Promise => { + setMutationError(undefined); + try { + await createAnnotation.mutateAsync({ + workspace, + data: { + kind: FeedbackAnnotationInputKind.feedback, + value, + session_id: sessionId, + span_id: spanId, + }, + }); + await refreshAnnotations(); + } catch (error) { + setMutationError(getAnnotationErrorMessage(error, 'Failed to save feedback.')); + } + }; + + const handleNoteChange = (event: ChangeEvent): void => { + if (!(event.target instanceof HTMLTextAreaElement)) return; + setNoteText(event.target.value); + }; + + const handleNoteSubmit = async (event: FormEvent): Promise => { + event.preventDefault(); + const text = noteText.trim(); + if (!text) return; + + setMutationError(undefined); + try { + await createAnnotation.mutateAsync({ + workspace, + data: { + kind: NoteAnnotationInputKind.note, + text, + session_id: sessionId, + span_id: spanId, + }, + }); + setNoteText(''); + await refreshAnnotations(); + } catch (error) { + setMutationError(getAnnotationErrorMessage(error, 'Failed to save note.')); + } + }; + + const handleDelete = async (annotationId: string): Promise => { + setMutationError(undefined); + try { + await deleteAnnotation.mutateAsync({ + workspace, + annotationId, + }); + await refreshAnnotations(); + } catch (error) { + setMutationError(getAnnotationErrorMessage(error, 'Failed to delete annotation.')); + } + }; + + return ( + } + slotHeading="Annotations" + className="min-w-0 overflow-hidden" + > + + + Feedback + + void handleFeedback(FeedbackAnnotationInputValue.positive)} + > + Positive + + void handleFeedback(FeedbackAnnotationInputValue.negative)} + > + Negative + + + + +
void handleNoteSubmit(event)}> + + +