From a329a4a98e34bd325f95ca3144589522a91bbf09 Mon Sep 17 00:00:00 2001 From: Inna Glamazda Date: Thu, 6 Aug 2026 12:54:43 -0400 Subject: [PATCH 1/8] refactor(evals): extract example dataset payloads from ProjectDatasets Move static download-example JSON into ProjectDatasets/exampleDatasetPayloads.ts so the page stays focused on orchestration. Co-authored-by: Cursor --- .../pages/EvalsDashboard/ProjectDatasets.tsx | 201 +--------------- .../ProjectDatasets/exampleDatasetPayloads.ts | 215 ++++++++++++++++++ 2 files changed, 217 insertions(+), 199 deletions(-) create mode 100644 Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/exampleDatasetPayloads.ts diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx index 8a38344662..c2bb0abfc5 100644 --- a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx @@ -73,6 +73,7 @@ import HelperIcon from "../../components/HelperIcon"; import TipBox from "../../components/TipBox"; import SelectableCard from "../../components/SelectableCard"; import { useAuth } from "../../../application/hooks/useAuth"; +import { getExampleDatasetPayload } from "./ProjectDatasets/exampleDatasetPayloads"; import allowedRoles from "../../../application/constants/permissions"; type ProjectDatasetsProps = { projectId: string; orgId?: string | null }; @@ -658,205 +659,7 @@ export function ProjectDatasets({ projectId, orgId }: ProjectDatasetsProps) { >("single-turn"); const handleDownloadExample = (type: "chatbot" | "rag" | "agent" = exampleDatasetType) => { - // Single-turn examples - const singleTurnDatasets = { - chatbot: [ - { - id: "chatbot_001", - category: "general_knowledge", - prompt: "What is the capital of France?", - expected_output: "The capital of France is Paris.", - expected_keywords: ["Paris", "capital", "France"], - difficulty: "easy", - }, - { - id: "chatbot_002", - category: "coding", - prompt: "Write a Python function to reverse a string.", - expected_output: "def reverse_string(s):\n return s[::-1]", - expected_keywords: ["def", "return"], - difficulty: "medium", - }, - ], - rag: [ - { - id: "rag_001", - category: "document_qa", - prompt: "What are the key benefits of renewable energy?", - expected_output: - "The key benefits include reduced carbon emissions and energy independence.", - expected_keywords: ["carbon emissions", "energy independence"], - difficulty: "medium", - retrieval_context: [ - "Renewable energy sources offer significant environmental benefits by reducing greenhouse gas emissions.", - "Countries that invest in renewable energy achieve greater energy independence.", - ], - }, - ], - agent: [ - { - id: "agent_001", - category: "task_execution", - prompt: "Search for the weather in New York and summarize it.", - expected_output: - "I searched for the current weather in New York. The temperature is 72°F.", - expected_keywords: ["weather", "New York", "temperature"], - difficulty: "medium", - tools_available: ["web_search", "calculator", "calendar"], - expected_tools: ["web_search"], - }, - ], - }; - - // Multi-turn examples - const multiTurnDatasets = { - chatbot: [ - { - scenario: "Customer asking about product features", - expected_outcome: "Successfully explain product features and answer follow-up questions", - turns: [ - { - role: "user", - content: "Hi, I'm interested in your premium plan. What features does it include?", - }, - { - role: "assistant", - content: - "Hello! Our premium plan includes unlimited storage, priority support, and advanced analytics. Would you like details on any specific feature?", - }, - { role: "user", content: "Yes, tell me more about the advanced analytics." }, - { - role: "assistant", - content: - "Our advanced analytics provides real-time dashboards, custom reports, and predictive insights powered by AI.", - }, - ], - }, - { - scenario: "Technical troubleshooting conversation", - expected_outcome: "Guide user through troubleshooting steps", - turns: [ - { role: "user", content: "My app keeps crashing when I try to upload files." }, - { - role: "assistant", - content: - "I'm sorry to hear that. Let me help you troubleshoot. What type of files are you trying to upload, and what's their size?", - }, - { role: "user", content: "PDFs, around 50MB each." }, - { - role: "assistant", - content: - "That file size should work fine. Can you try clearing your browser cache and attempting the upload again?", - }, - ], - }, - ], - rag: [ - { - scenario: "Document-based Q&A about company policies", - expected_outcome: "Accurately answer questions using retrieved context", - context: [ - "Employees are entitled to 20 days of paid time off per year.", - "Remote work is permitted up to 3 days per week with manager approval.", - ], - turns: [ - { role: "user", content: "How many vacation days do I get per year?" }, - { - role: "assistant", - content: - "According to the company policy, employees are entitled to 20 days of paid time off per year.", - }, - { role: "user", content: "Can I work from home?" }, - { - role: "assistant", - content: - "Yes, remote work is permitted up to 3 days per week, but you'll need your manager's approval.", - }, - ], - }, - ], - agent: [ - { - scenario: "Planning a trip with multiple tools", - expected_outcome: "Successfully use tools to help plan a trip", - tools_available: ["web_search", "calendar", "weather_api"], - turns: [ - { role: "user", content: "Help me plan a trip to Paris next month." }, - { - role: "assistant", - content: - "I'd be happy to help! Let me check the weather forecast for Paris next month. [uses weather_api]", - }, - { role: "user", content: "What are the must-see attractions?" }, - { - role: "assistant", - content: - "Let me search for top Paris attractions. [uses web_search] The top attractions include the Eiffel Tower, Louvre Museum, and Notre-Dame Cathedral.", - }, - ], - }, - ], - }; - - // Simulated examples (scenario-only, no turns - AI generates the conversation) - const simulatedDatasets = { - chatbot: [ - { - scenario: "User wants to book a flight to Paris", - expected_outcome: - "Successfully complete flight booking with date, class, and seat preference confirmed", - user_description: "Frequent business traveler, prefers aisle seats, flexible on dates", - max_turns: 8, - }, - { - scenario: "Customer complaining about a defective product", - expected_outcome: - "Resolve complaint with appropriate compensation (refund or replacement)", - user_description: - "Frustrated customer who bought the item last week, wants quick resolution", - max_turns: 6, - }, - { - scenario: "New user asking for help getting started with the platform", - expected_outcome: "User understands core features and can navigate the dashboard", - user_description: "First-time user, not very tech-savvy, prefers step-by-step guidance", - }, - ], - rag: [ - { - scenario: "Employee asking HR questions about benefits and policies", - expected_outcome: "Provide accurate information from company documents", - user_description: "New employee unfamiliar with company policies", - max_turns: 8, - }, - { - scenario: "User researching a technical topic using documentation", - expected_outcome: "Synthesize information from multiple documents accurately", - user_description: "Developer looking for API integration guidance", - }, - ], - agent: [ - { - scenario: "User planning a multi-city vacation with budget constraints", - expected_outcome: "Create complete itinerary using search, calendar, and weather tools", - user_description: - "Budget-conscious traveler, flexible dates, prefers cultural experiences", - max_turns: 10, - }, - { - scenario: "Manager scheduling a team meeting across time zones", - expected_outcome: "Find optimal meeting time using calendar integration", - user_description: "Busy manager with team members in 3 different time zones", - }, - ], - }; - - const exampleData = - datasetTurnType === "single-turn" - ? singleTurnDatasets[type] - : datasetTurnType === "multi-turn" - ? multiTurnDatasets[type] - : simulatedDatasets[type]; + const exampleData = getExampleDatasetPayload(datasetTurnType, type); const filename = `example_${datasetTurnType}_${type}_dataset.json`; const blob = new Blob([JSON.stringify(exampleData, null, 2)], { type: "application/json" }); diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/exampleDatasetPayloads.ts b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/exampleDatasetPayloads.ts new file mode 100644 index 0000000000..fc95e3b27d --- /dev/null +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/exampleDatasetPayloads.ts @@ -0,0 +1,215 @@ +/** + * @fileoverview Static example JSON payloads for the Project Datasets + * "Download example" action in the upload instructions modal. + * + * @module pages/EvalsDashboard/ProjectDatasets/exampleDatasetPayloads + */ + +export type ExampleUseCase = "chatbot" | "rag" | "agent"; + +export type ExampleTurnType = "single-turn" | "multi-turn" | "simulated"; + +/** Single-turn example rows keyed by use case. */ +export const SINGLE_TURN_EXAMPLE_DATASETS = { + chatbot: [ + { + id: "chatbot_001", + category: "general_knowledge", + prompt: "What is the capital of France?", + expected_output: "The capital of France is Paris.", + expected_keywords: ["Paris", "capital", "France"], + difficulty: "easy", + }, + { + id: "chatbot_002", + category: "coding", + prompt: "Write a Python function to reverse a string.", + expected_output: "def reverse_string(s):\n return s[::-1]", + expected_keywords: ["def", "return"], + difficulty: "medium", + }, + ], + rag: [ + { + id: "rag_001", + category: "document_qa", + prompt: "What are the key benefits of renewable energy?", + expected_output: "The key benefits include reduced carbon emissions and energy independence.", + expected_keywords: ["carbon emissions", "energy independence"], + difficulty: "medium", + retrieval_context: [ + "Renewable energy sources offer significant environmental benefits by reducing greenhouse gas emissions.", + "Countries that invest in renewable energy achieve greater energy independence.", + ], + }, + ], + agent: [ + { + id: "agent_001", + category: "task_execution", + prompt: "Search for the weather in New York and summarize it.", + expected_output: "I searched for the current weather in New York. The temperature is 72°F.", + expected_keywords: ["weather", "New York", "temperature"], + difficulty: "medium", + tools_available: ["web_search", "calculator", "calendar"], + expected_tools: ["web_search"], + }, + ], +} as const; + +/** Multi-turn conversation examples keyed by use case. */ +export const MULTI_TURN_EXAMPLE_DATASETS = { + chatbot: [ + { + scenario: "Customer asking about product features", + expected_outcome: "Successfully explain product features and answer follow-up questions", + turns: [ + { + role: "user", + content: "Hi, I'm interested in your premium plan. What features does it include?", + }, + { + role: "assistant", + content: + "Hello! Our premium plan includes unlimited storage, priority support, and advanced analytics. Would you like details on any specific feature?", + }, + { role: "user", content: "Yes, tell me more about the advanced analytics." }, + { + role: "assistant", + content: + "Our advanced analytics provides real-time dashboards, custom reports, and predictive insights powered by AI.", + }, + ], + }, + { + scenario: "Technical troubleshooting conversation", + expected_outcome: "Guide user through troubleshooting steps", + turns: [ + { role: "user", content: "My app keeps crashing when I try to upload files." }, + { + role: "assistant", + content: + "I'm sorry to hear that. Let me help you troubleshoot. What type of files are you trying to upload, and what's their size?", + }, + { role: "user", content: "PDFs, around 50MB each." }, + { + role: "assistant", + content: + "That file size should work fine. Can you try clearing your browser cache and attempting the upload again?", + }, + ], + }, + ], + rag: [ + { + scenario: "Document-based Q&A about company policies", + expected_outcome: "Accurately answer questions using retrieved context", + context: [ + "Employees are entitled to 20 days of paid time off per year.", + "Remote work is permitted up to 3 days per week with manager approval.", + ], + turns: [ + { role: "user", content: "How many vacation days do I get per year?" }, + { + role: "assistant", + content: + "According to the company policy, employees are entitled to 20 days of paid time off per year.", + }, + { role: "user", content: "Can I work from home?" }, + { + role: "assistant", + content: + "Yes, remote work is permitted up to 3 days per week, but you'll need your manager's approval.", + }, + ], + }, + ], + agent: [ + { + scenario: "Planning a trip with multiple tools", + expected_outcome: "Successfully use tools to help plan a trip", + tools_available: ["web_search", "calendar", "weather_api"], + turns: [ + { role: "user", content: "Help me plan a trip to Paris next month." }, + { + role: "assistant", + content: + "I'd be happy to help! Let me check the weather forecast for Paris next month. [uses weather_api]", + }, + { role: "user", content: "What are the must-see attractions?" }, + { + role: "assistant", + content: + "Let me search for top Paris attractions. [uses web_search] The top attractions include the Eiffel Tower, Louvre Museum, and Notre-Dame Cathedral.", + }, + ], + }, + ], +} as const; + +/** + * Simulated examples (scenario-only, no turns — AI generates the conversation). + * Keyed by use case. + */ +export const SIMULATED_EXAMPLE_DATASETS = { + chatbot: [ + { + scenario: "User wants to book a flight to Paris", + expected_outcome: + "Successfully complete flight booking with date, class, and seat preference confirmed", + user_description: "Frequent business traveler, prefers aisle seats, flexible on dates", + max_turns: 8, + }, + { + scenario: "Customer complaining about a defective product", + expected_outcome: "Resolve complaint with appropriate compensation (refund or replacement)", + user_description: "Frustrated customer who bought the item last week, wants quick resolution", + max_turns: 6, + }, + { + scenario: "New user asking for help getting started with the platform", + expected_outcome: "User understands core features and can navigate the dashboard", + user_description: "First-time user, not very tech-savvy, prefers step-by-step guidance", + }, + ], + rag: [ + { + scenario: "Employee asking HR questions about benefits and policies", + expected_outcome: "Provide accurate information from company documents", + user_description: "New employee unfamiliar with company policies", + max_turns: 8, + }, + { + scenario: "User researching a technical topic using documentation", + expected_outcome: "Synthesize information from multiple documents accurately", + user_description: "Developer looking for API integration guidance", + }, + ], + agent: [ + { + scenario: "User planning a multi-city vacation with budget constraints", + expected_outcome: "Create complete itinerary using search, calendar, and weather tools", + user_description: "Budget-conscious traveler, flexible dates, prefers cultural experiences", + max_turns: 10, + }, + { + scenario: "Manager scheduling a team meeting across time zones", + expected_outcome: "Find optimal meeting time using calendar integration", + user_description: "Busy manager with team members in 3 different time zones", + }, + ], +} as const; + +/** + * Resolve the example payload for a given turn type and use case. + * Used by the upload-modal "Download example" action. + */ +export const getExampleDatasetPayload = (turnType: ExampleTurnType, useCase: ExampleUseCase) => { + if (turnType === "single-turn") { + return SINGLE_TURN_EXAMPLE_DATASETS[useCase]; + } + if (turnType === "multi-turn") { + return MULTI_TURN_EXAMPLE_DATASETS[useCase]; + } + return SIMULATED_EXAMPLE_DATASETS[useCase]; +}; From 8d02983e498e5970778bf14125e06801643a4d74 Mon Sep 17 00:00:00 2001 From: Inna Glamazda Date: Thu, 6 Aug 2026 18:30:57 -0400 Subject: [PATCH 2/8] refactor(evals): extract UploadDatasetModal from ProjectDatasets Move the upload instructions modal (turn type, use case, JSON preview, download example) into ProjectDatasets/UploadDatasetModal.tsx. Co-authored-by: Cursor --- .../pages/EvalsDashboard/ProjectDatasets.tsx | 502 +---------------- .../ProjectDatasets/UploadDatasetModal.tsx | 505 ++++++++++++++++++ 2 files changed, 517 insertions(+), 490 deletions(-) create mode 100644 Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/UploadDatasetModal.tsx diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx index c2bb0abfc5..a84eee59c0 100644 --- a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx @@ -73,7 +73,8 @@ import HelperIcon from "../../components/HelperIcon"; import TipBox from "../../components/TipBox"; import SelectableCard from "../../components/SelectableCard"; import { useAuth } from "../../../application/hooks/useAuth"; -import { getExampleDatasetPayload } from "./ProjectDatasets/exampleDatasetPayloads"; +import UploadDatasetModal from "./ProjectDatasets/UploadDatasetModal"; +import type { ExampleTurnType, ExampleUseCase } from "./ProjectDatasets/exampleDatasetPayloads"; import allowedRoles from "../../../application/constants/permissions"; type ProjectDatasetsProps = { projectId: string; orgId?: string | null }; @@ -650,28 +651,9 @@ export function ProjectDatasets({ projectId, orgId }: ProjectDatasetsProps) { fileInputRef.current?.click(); }; - // Example dataset type for download - const [exampleDatasetType, setExampleDatasetType] = useState<"chatbot" | "rag" | "agent">( - "chatbot", - ); - const [datasetTurnType, setDatasetTurnType] = useState< - "single-turn" | "multi-turn" | "simulated" - >("single-turn"); - - const handleDownloadExample = (type: "chatbot" | "rag" | "agent" = exampleDatasetType) => { - const exampleData = getExampleDatasetPayload(datasetTurnType, type); - const filename = `example_${datasetTurnType}_${type}_dataset.json`; - - const blob = new Blob([JSON.stringify(exampleData, null, 2)], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; + // Example dataset type for download / upload metadata + const [exampleDatasetType, setExampleDatasetType] = useState("chatbot"); + const [datasetTurnType, setDatasetTurnType] = useState("single-turn"); const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -1913,475 +1895,15 @@ export function ProjectDatasets({ projectId, orgId }: ProjectDatasetsProps) { proceedButtonVariant="contained" /> - {/* Upload instructions modal */} - setUploadModalOpen(false)} - title="Upload dataset" - description="Upload a custom dataset in JSON format for your evaluations" - customFooter={ - - setUploadModalOpen(false)} - sx={{ - "minWidth": "80px", - "height": "34px", - "border": `1px solid ${palette.border.dark}`, - "color": palette.text.secondary, - "&:hover": { - backgroundColor: palette.background.accent, - border: `1px solid ${palette.border.dark}`, - }, - }} - /> - } - sx={{ - "minWidth": "120px", - "height": "34px", - "backgroundColor": palette.brand.primary, - "&:hover": { - backgroundColor: palette.brand.primaryHover, - }, - }} - /> - - } - > - - {/* Turn type selector - NEW */} - - - Conversation type - - - setDatasetTurnType("single-turn")} sx={{ cursor: "pointer" }}> - - - setDatasetTurnType("multi-turn")} sx={{ cursor: "pointer" }}> - - - - - {/* Multi-turn sub-options: Default or Simulated */} - {(datasetTurnType === "multi-turn" || datasetTurnType === "simulated") && ( - - - Multi-turn mode: - - - setDatasetTurnType("multi-turn")} sx={{ cursor: "pointer" }}> - - - setDatasetTurnType("simulated")} sx={{ cursor: "pointer" }}> - - - - - )} - - - {datasetTurnType === "single-turn" - ? "Simple prompt → response pairs. Best for RAG and basic Q&A evaluation." - : datasetTurnType === "multi-turn" - ? "Multi-turn conversations with scenario and turns. Best for chatbot and agent evaluation." - : "Define scenarios only — the AI will simulate full conversations dynamically during evaluation."} - - - - {/* Dataset type selector */} - - - Use case - - - {/* Agent evaluation now supported per DeepEval docs */} - {(["chatbot", "rag", "agent"] as const).map((type) => { - const isSelected = exampleDatasetType === type; - return ( - setExampleDatasetType(type)} - sx={{ cursor: "pointer" }} - > - - - ); - })} - - - {exampleDatasetType === "chatbot" && - "Standard Q&A datasets for evaluating chatbot responses."} - {exampleDatasetType === "rag" && - "Datasets with retrieval_context for RAG faithfulness & relevancy metrics."} - {exampleDatasetType === "agent" && - "Datasets with tools_available for evaluating agent reasoning, tool usage, and task completion."} - - - - {/* JSON structure based on turn type */} - - - - {datasetTurnType === "single-turn" - ? "Single-Turn" - : datasetTurnType === "multi-turn" - ? "Multi-Turn" - : "Simulated"}{" "} - JSON format - - } - onClick={() => handleDownloadExample(exampleDatasetType)} - text="Download example" - sx={{ - "fontSize": "12px", - "color": palette.brand.primary, - "&:hover": { - backgroundColor: "rgba(19, 113, 91, 0.08)", - }, - }} - /> - - -
-                {datasetTurnType === "single-turn"
-                  ? `[
-  {
-    "id": "prompt_001",
-    "category": "general",
-    "prompt": "What is machine learning?",
-    "expected_output": "Machine learning is...",
-    "expected_keywords": ["algorithm", "data"],
-    "difficulty": "easy"${
-      exampleDatasetType === "rag"
-        ? `,
-    "retrieval_context": [
-      "Context document 1...",
-      "Context document 2..."
-    ]`
-        : exampleDatasetType === "agent"
-          ? `,
-    "tools_available": ["web_search"],
-    "expected_tools": ["web_search"]`
-          : ""
-    }
-  }
-]`
-                  : datasetTurnType === "multi-turn"
-                    ? `[
-  {
-    "scenario": "Customer asking for help",
-    "expected_outcome": "Successfully assist customer",${
-      exampleDatasetType === "rag"
-        ? `
-    "context": ["Relevant document..."],`
-        : ""
-    }${
-      exampleDatasetType === "agent"
-        ? `
-    "tools_available": ["search", "calendar"],`
-        : ""
-    }
-    "turns": [
-      { "role": "user", "content": "Hi, I need help" },
-      { "role": "assistant", "content": "Hello! How can I assist you today?" },
-      { "role": "user", "content": "I have a question about..." },
-      { "role": "assistant", "content": "I'd be happy to help with that." }
-    ]
-  }
-]`
-                    : `[
-  {
-    "scenario": "User wants to book a flight to Paris",
-    "expected_outcome": "Successfully complete flight booking",
-    "user_description": "Frequent traveler, prefers window seats",
-    "max_turns": 8
-  },
-  {
-    "scenario": "Customer complaining about a defective product",
-    "expected_outcome": "Resolve complaint with refund or replacement",
-    "user_description": "Frustrated customer, bought item last week"
-  }
-]`}
-              
-
- {datasetTurnType === "simulated" && ( - - - How Simulated Mode Works - - - You provide scenarios only — no need to write conversations. During evaluation, - the AI will: -
• Simulate a user based on your description -
• Generate realistic multi-turn conversations -
• Evaluate the assistant's responses automatically -
-
- )} -
- - {/* Field descriptions based on turn type */} - - - {datasetTurnType === "single-turn" - ? "Single-Turn" - : datasetTurnType === "multi-turn" - ? "Multi-Turn" - : "Simulated"}{" "} - fields - - - - - id - - - (required) Unique identifier - - - - - prompt - - - (required) The input question or task - - - - - expected_output - - - (required) Expected model response - - - {exampleDatasetType === "rag" && ( - - - retrieval_context - - - (required for RAG) Array of retrieved context documents - - - )} - {/* Agent not supported yet */} - {/* {exampleDatasetType === "agent" && ( - <> - - - scenario - - - (required) Description of the conversation scenario - - - - - turns - - - (required) Array of {"{ role, content }"} messages - - - - - expected_outcome - - - (optional) Expected result of the conversation - - - - ) : ( - <> - - - scenario - - - (required) Description of what the user wants to accomplish - - - - - expected_outcome - - - (required) What counts as a successful conversation - - - - - user_description - - - (optional) Persona for the simulated user - - - - - max_turns - - - (optional) Maximum turns to simulate (default: 6) - - - - )} */} - - -
-
+ turnType={datasetTurnType} + onTurnTypeChange={setDatasetTurnType} + useCase={exampleDatasetType} + onUseCaseChange={setExampleDatasetType} + onUploadClick={handleFileSelect} + /> {/* Dataset Content Drawer */} diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/UploadDatasetModal.tsx b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/UploadDatasetModal.tsx new file mode 100644 index 0000000000..db5b0277d7 --- /dev/null +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/UploadDatasetModal.tsx @@ -0,0 +1,505 @@ +/** + * @fileoverview Upload-dataset instructions modal: turn type, use case, + * JSON format preview, example download, and file-picker CTA. + * + * @module pages/EvalsDashboard/ProjectDatasets/UploadDatasetModal + */ + +import { Box, Stack, Typography } from "@mui/material"; +import { Download, Upload } from "lucide-react"; +import { CustomizableButton } from "../../../components/button/customizable-button"; +import Chip from "../../../components/Chip"; +import ModalStandard from "../../../components/Modals/StandardModal"; +import { palette } from "../../../themes/palette"; +import { + getExampleDatasetPayload, + type ExampleTurnType, + type ExampleUseCase, +} from "./exampleDatasetPayloads"; + +export type UploadDatasetModalProps = { + isOpen: boolean; + onClose: () => void; + turnType: ExampleTurnType; + onTurnTypeChange: (turnType: ExampleTurnType) => void; + useCase: ExampleUseCase; + onUseCaseChange: (useCase: ExampleUseCase) => void; + onUploadClick: () => void; +}; + +const handleDownloadExample = (turnType: ExampleTurnType, useCase: ExampleUseCase) => { + const exampleData = getExampleDatasetPayload(turnType, useCase); + const filename = `example_${turnType}_${useCase}_dataset.json`; + + const blob = new Blob([JSON.stringify(exampleData, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +}; + +export default function UploadDatasetModal({ + isOpen, + onClose, + turnType, + onTurnTypeChange, + useCase, + onUseCaseChange, + onUploadClick, +}: UploadDatasetModalProps) { + return ( + + + } + sx={{ + "minWidth": "120px", + "height": "34px", + "backgroundColor": palette.brand.primary, + "&:hover": { + backgroundColor: palette.brand.primaryHover, + }, + }} + /> + + } + > + + {/* Turn type selector - NEW */} + + + Conversation type + + + onTurnTypeChange("single-turn")} sx={{ cursor: "pointer" }}> + + + onTurnTypeChange("multi-turn")} sx={{ cursor: "pointer" }}> + + + + + {/* Multi-turn sub-options: Default or Simulated */} + {(turnType === "multi-turn" || turnType === "simulated") && ( + + + Multi-turn mode: + + + onTurnTypeChange("multi-turn")} sx={{ cursor: "pointer" }}> + + + onTurnTypeChange("simulated")} sx={{ cursor: "pointer" }}> + + + + + )} + + + {turnType === "single-turn" + ? "Simple prompt → response pairs. Best for RAG and basic Q&A evaluation." + : turnType === "multi-turn" + ? "Multi-turn conversations with scenario and turns. Best for chatbot and agent evaluation." + : "Define scenarios only — the AI will simulate full conversations dynamically during evaluation."} + + + + {/* Dataset type selector */} + + + Use case + + + {/* Agent evaluation now supported per DeepEval docs */} + {(["chatbot", "rag", "agent"] as const).map((type) => { + const isSelected = useCase === type; + return ( + onUseCaseChange(type)} sx={{ cursor: "pointer" }}> + + + ); + })} + + + {useCase === "chatbot" && "Standard Q&A datasets for evaluating chatbot responses."} + {useCase === "rag" && + "Datasets with retrieval_context for RAG faithfulness & relevancy metrics."} + {useCase === "agent" && + "Datasets with tools_available for evaluating agent reasoning, tool usage, and task completion."} + + + + {/* JSON structure based on turn type */} + + + + {turnType === "single-turn" + ? "Single-Turn" + : turnType === "multi-turn" + ? "Multi-Turn" + : "Simulated"}{" "} + JSON format + + } + onClick={() => handleDownloadExample(turnType, useCase)} + text="Download example" + sx={{ + "fontSize": "12px", + "color": palette.brand.primary, + "&:hover": { + backgroundColor: "rgba(19, 113, 91, 0.08)", + }, + }} + /> + + +
+              {turnType === "single-turn"
+                ? `[
+  {
+    "id": "prompt_001",
+    "category": "general",
+    "prompt": "What is machine learning?",
+    "expected_output": "Machine learning is...",
+    "expected_keywords": ["algorithm", "data"],
+    "difficulty": "easy"${
+      useCase === "rag"
+        ? `,
+    "retrieval_context": [
+      "Context document 1...",
+      "Context document 2..."
+    ]`
+        : useCase === "agent"
+          ? `,
+    "tools_available": ["web_search"],
+    "expected_tools": ["web_search"]`
+          : ""
+    }
+  }
+]`
+                : turnType === "multi-turn"
+                  ? `[
+  {
+    "scenario": "Customer asking for help",
+    "expected_outcome": "Successfully assist customer",${
+      useCase === "rag"
+        ? `
+    "context": ["Relevant document..."],`
+        : ""
+    }${
+      useCase === "agent"
+        ? `
+    "tools_available": ["search", "calendar"],`
+        : ""
+    }
+    "turns": [
+      { "role": "user", "content": "Hi, I need help" },
+      { "role": "assistant", "content": "Hello! How can I assist you today?" },
+      { "role": "user", "content": "I have a question about..." },
+      { "role": "assistant", "content": "I'd be happy to help with that." }
+    ]
+  }
+]`
+                  : `[
+  {
+    "scenario": "User wants to book a flight to Paris",
+    "expected_outcome": "Successfully complete flight booking",
+    "user_description": "Frequent traveler, prefers window seats",
+    "max_turns": 8
+  },
+  {
+    "scenario": "Customer complaining about a defective product",
+    "expected_outcome": "Resolve complaint with refund or replacement",
+    "user_description": "Frustrated customer, bought item last week"
+  }
+]`}
+            
+
+ {turnType === "simulated" && ( + + + How Simulated Mode Works + + + You provide scenarios only — no need to write conversations. During evaluation, the + AI will: +
• Simulate a user based on your description +
• Generate realistic multi-turn conversations +
• Evaluate the assistant's responses automatically +
+
+ )} +
+ + {/* Field descriptions based on turn type */} + + + {turnType === "single-turn" + ? "Single-Turn" + : turnType === "multi-turn" + ? "Multi-Turn" + : "Simulated"}{" "} + fields + + + + + id + + + (required) Unique identifier + + + + + prompt + + + (required) The input question or task + + + + + expected_output + + + (required) Expected model response + + + {useCase === "rag" && ( + + + retrieval_context + + + (required for RAG) Array of retrieved context documents + + + )} + {/* Agent not supported yet */} + {/* {useCase === "agent" && ( + <> + + + scenario + + + (required) Description of the conversation scenario + + + + + turns + + + (required) Array of {"{ role, content }"} messages + + + + + expected_outcome + + + (optional) Expected result of the conversation + + + + ) : ( + <> + + + scenario + + + (required) Description of what the user wants to accomplish + + + + + expected_outcome + + + (required) What counts as a successful conversation + + + + + user_description + + + (optional) Persona for the simulated user + + + + + max_turns + + + (optional) Maximum turns to simulate (default: 6) + + + + )} */} + + +
+
+ ); +} From 01c7df0063a9f8e29d6cd4d6a9242bfcf4fb96a4 Mon Sep 17 00:00:00 2001 From: Inna Glamazda Date: Thu, 6 Aug 2026 19:05:25 -0400 Subject: [PATCH 3/8] refactor(evals): extract CreateDatasetModals from ProjectDatasets Move the add-dataset choice and format-selection modals into a dedicated component to continue splitting the oversized ProjectDatasets page. Co-authored-by: Cursor --- .../pages/EvalsDashboard/ProjectDatasets.tsx | 242 ++--------------- .../ProjectDatasets/CreateDatasetModals.tsx | 243 ++++++++++++++++++ 2 files changed, 267 insertions(+), 218 deletions(-) create mode 100644 Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/CreateDatasetModals.tsx diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx index a84eee59c0..dc5f8ca13c 100644 --- a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx @@ -30,8 +30,6 @@ import { User, Bot, Check, - MessageSquare, - GitBranch, Eye, } from "lucide-react"; import { CustomizableButton } from "../../components/button/customizable-button"; @@ -55,7 +53,6 @@ import { } from "../../../application/repository/deepEval.repository"; import Alert from "../../components/Alert"; import Chip from "../../components/Chip"; -import ModalStandard from "../../components/Modals/StandardModal"; import ConfirmationModal from "../../components/Dialogs/ConfirmationModal"; import Field from "../../components/Inputs/Field"; import SearchBox from "../../components/Search/SearchBox"; @@ -71,9 +68,9 @@ import TemplatesTable from "../../components/Table/TemplatesTable"; import { PageHeader } from "../../components/Layout/PageHeader"; import HelperIcon from "../../components/HelperIcon"; import TipBox from "../../components/TipBox"; -import SelectableCard from "../../components/SelectableCard"; import { useAuth } from "../../../application/hooks/useAuth"; import UploadDatasetModal from "./ProjectDatasets/UploadDatasetModal"; +import CreateDatasetModals from "./ProjectDatasets/CreateDatasetModals"; import type { ExampleTurnType, ExampleUseCase } from "./ProjectDatasets/exampleDatasetPayloads"; import allowedRoles from "../../../application/constants/permissions"; @@ -174,13 +171,7 @@ export function ProjectDatasets({ projectId, orgId }: ProjectDatasetsProps) { // Create dataset modal state const [createDatasetModalOpen, setCreateDatasetModalOpen] = useState(false); - - // Create from scratch type selection modal const [createTypeSelectionOpen, setCreateTypeSelectionOpen] = useState(false); - const [newDatasetUseCase, setNewDatasetUseCase] = useState<"chatbot" | "rag">("chatbot"); - const [newDatasetTurnType, setNewDatasetTurnType] = useState<"single-turn" | "multi-turn">( - "single-turn", - ); // Note: promptCount is now returned by the API - no need to load metadata individually @@ -2619,222 +2610,37 @@ export function ProjectDatasets({ projectId, orgId }: ProjectDatasetsProps) {
- {/* Create Dataset Modal - Choice between Editor and Upload */} - setCreateDatasetModalOpen(false)} - title="Add dataset" - description="Choose how you want to add a new dataset" - maxWidth="480px" - > - - {/* Create from scratch option */} - { - setCreateDatasetModalOpen(false); - setCreateTypeSelectionOpen(true); - }} - icon={} - title="Create from scratch" - description="Choose format and manually add prompts" - /> - - {/* Upload JSON option */} - { - setCreateDatasetModalOpen(false); - setUploadModalOpen(true); - }} - icon={} - title="Upload JSON file" - description="Import existing dataset in JSON format" - /> - - {/* Use template option */} - { - setCreateDatasetModalOpen(false); - setActiveTab("templates"); - }} - icon={} - title="Start from template" - description="Browse pre-built evaluation templates" - /> - - - - {/* Create from scratch - Type Selection Modal */} - setCreateTypeSelectionOpen(false)} - title="Choose dataset format" - description="Select the type and format for your new dataset" - maxWidth="520px" - submitButtonText="Create Dataset" - onSubmit={() => { - setCreateTypeSelectionOpen(false); - - // Initialize with appropriate format based on selection - if (newDatasetTurnType === "single-turn") { - const singleTurnPrompt: SingleTurnPrompt = { - id: "prompt_1", - category: "general", - prompt: "", - expected_output: "", - difficulty: "medium", - ...(newDatasetUseCase === "rag" ? { retrieval_context: [] } : {}), - }; - setEditablePrompts([singleTurnPrompt]); - } else { - const multiTurnConversation: MultiTurnConversation = { - id: "conversation_1", - scenario: "", - expected_outcome: "", - turns: [{ role: "user", content: "" }], - ...(newDatasetUseCase === "rag" ? { context: [] } : {}), - }; - setEditablePrompts([multiTurnConversation]); - } - + setCreateDatasetModalOpen(false)} + onOpenTypeSelection={() => { + setCreateDatasetModalOpen(false); + setCreateTypeSelectionOpen(true); + }} + onChooseUpload={() => { + setCreateDatasetModalOpen(false); + setUploadModalOpen(true); + }} + onChooseTemplate={() => { + setCreateDatasetModalOpen(false); + setActiveTab("templates"); + }} + typeSelectionOpen={createTypeSelectionOpen} + onTypeSelectionClose={() => setCreateTypeSelectionOpen(false)} + onCreate={(draft) => { + setEditablePrompts(draft.prompts); setEditDatasetName(""); setEditingDataset({ key: "new", name: "New Dataset", path: "", - use_case: newDatasetUseCase, - datasetType: newDatasetUseCase, - turnType: newDatasetTurnType, + use_case: draft.useCase, + datasetType: draft.useCase, + turnType: draft.turnType, }); setEditorOpen(true); }} - > - - {/* Use Case Selection */} - - - Use Case - - - setNewDatasetUseCase("chatbot")} - sx={{ cursor: "pointer", flex: 1 }} - > - setNewDatasetUseCase("chatbot")} - icon={ - - } - title="Chatbot" - description="Standard Q&A evaluation" - /> - - setNewDatasetUseCase("rag")} sx={{ cursor: "pointer", flex: 1 }}> - setNewDatasetUseCase("rag")} - icon={ - - } - title="RAG" - description="With retrieval context" - /> - - - - - {/* Turn Type Selection */} - - - Conversation Format - - - setNewDatasetTurnType("single-turn")} - sx={{ cursor: "pointer", flex: 1 }} - > - setNewDatasetTurnType("single-turn")} - icon={ - - } - title="Single-turn" - description="One prompt, one response" - /> - - setNewDatasetTurnType("multi-turn")} - sx={{ cursor: "pointer", flex: 1 }} - > - setNewDatasetTurnType("multi-turn")} - icon={ - - } - title="Multi-turn" - description="Conversation with multiple exchanges" - /> - - - - - {/* Format Preview */} - - - Format Preview - - - {newDatasetTurnType === "single-turn" - ? newDatasetUseCase === "rag" - ? "Prompts with expected output, category, difficulty, and retrieval_context fields" - : "Prompts with expected output, category, and difficulty fields" - : newDatasetUseCase === "rag" - ? "Conversations with scenario, multiple turns (user/assistant), expected outcome, and context" - : "Conversations with scenario, multiple turns (user/assistant), and expected outcome"} - - - - + /> ); } diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/CreateDatasetModals.tsx b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/CreateDatasetModals.tsx new file mode 100644 index 0000000000..a5169b6f5e --- /dev/null +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/CreateDatasetModals.tsx @@ -0,0 +1,243 @@ +/** + * @fileoverview Create-dataset flow modals: choose how to add a dataset, + * then pick use case + conversation format when creating from scratch. + * + * @module pages/EvalsDashboard/ProjectDatasets/CreateDatasetModals + */ + +import { useState } from "react"; +import { Box, Stack, Typography } from "@mui/material"; +import { Database, Edit3, GitBranch, MessageSquare, Upload } from "lucide-react"; +import SelectableCard from "../../../components/SelectableCard"; +import ModalStandard from "../../../components/Modals/StandardModal"; +import { palette } from "../../../themes/palette"; +import type { + MultiTurnConversation, + SingleTurnPrompt, +} from "../../../../application/repository/deepEval.repository"; + +export type NewDatasetUseCase = "chatbot" | "rag"; + +export type NewDatasetTurnType = "single-turn" | "multi-turn"; + +export type CreateDatasetDraft = { + useCase: NewDatasetUseCase; + turnType: NewDatasetTurnType; + prompts: Array; +}; + +export type CreateDatasetModalsProps = { + choiceOpen: boolean; + onChoiceClose: () => void; + onOpenTypeSelection: () => void; + onChooseUpload: () => void; + onChooseTemplate: () => void; + typeSelectionOpen: boolean; + onTypeSelectionClose: () => void; + onCreate: (draft: CreateDatasetDraft) => void; +}; + +const buildInitialPrompts = ( + useCase: NewDatasetUseCase, + turnType: NewDatasetTurnType, +): Array => { + if (turnType === "single-turn") { + const singleTurnPrompt: SingleTurnPrompt = { + id: "prompt_1", + category: "general", + prompt: "", + expected_output: "", + difficulty: "medium", + ...(useCase === "rag" ? { retrieval_context: [] } : {}), + }; + return [singleTurnPrompt]; + } + + const multiTurnConversation: MultiTurnConversation = { + id: "conversation_1", + scenario: "", + expected_outcome: "", + turns: [{ role: "user", content: "" }], + ...(useCase === "rag" ? { context: [] } : {}), + }; + return [multiTurnConversation]; +}; + +export default function CreateDatasetModals({ + choiceOpen, + onChoiceClose, + onOpenTypeSelection, + onChooseUpload, + onChooseTemplate, + typeSelectionOpen, + onTypeSelectionClose, + onCreate, +}: CreateDatasetModalsProps) { + const [useCase, setUseCase] = useState("chatbot"); + const [turnType, setTurnType] = useState("single-turn"); + + return ( + <> + {/* Create Dataset Modal - Choice between Editor and Upload */} + + + {/* Create from scratch option */} + } + title="Create from scratch" + description="Choose format and manually add prompts" + /> + + {/* Upload JSON option */} + } + title="Upload JSON file" + description="Import existing dataset in JSON format" + /> + + {/* Use template option */} + } + title="Start from template" + description="Browse pre-built evaluation templates" + /> + + + + {/* Create from scratch - Type Selection Modal */} + { + onTypeSelectionClose(); + onCreate({ + useCase, + turnType, + prompts: buildInitialPrompts(useCase, turnType), + }); + }} + > + + {/* Use Case Selection */} + + + Use Case + + + setUseCase("chatbot")} sx={{ cursor: "pointer", flex: 1 }}> + setUseCase("chatbot")} + icon={ + + } + title="Chatbot" + description="Standard Q&A evaluation" + /> + + setUseCase("rag")} sx={{ cursor: "pointer", flex: 1 }}> + setUseCase("rag")} + icon={ + + } + title="RAG" + description="With retrieval context" + /> + + + + + {/* Turn Type Selection */} + + + Conversation Format + + + setTurnType("single-turn")} sx={{ cursor: "pointer", flex: 1 }}> + setTurnType("single-turn")} + icon={ + + } + title="Single-turn" + description="One prompt, one response" + /> + + setTurnType("multi-turn")} sx={{ cursor: "pointer", flex: 1 }}> + setTurnType("multi-turn")} + icon={ + + } + title="Multi-turn" + description="Conversation with multiple exchanges" + /> + + + + + {/* Format Preview */} + + + Format Preview + + + {turnType === "single-turn" + ? useCase === "rag" + ? "Prompts with expected output, category, difficulty, and retrieval_context fields" + : "Prompts with expected output, category, and difficulty fields" + : useCase === "rag" + ? "Conversations with scenario, multiple turns (user/assistant), expected outcome, and context" + : "Conversations with scenario, multiple turns (user/assistant), and expected outcome"} + + + + + + ); +} From 4fdda70cff245bdec1141f3da747fbfd80eaa300 Mon Sep 17 00:00:00 2001 From: Inna Glamazda Date: Thu, 6 Aug 2026 19:22:42 -0400 Subject: [PATCH 4/8] refactor(evals): extract dataset and template preview drawers Move read-only My datasets and Templates preview drawers into ProjectDatasets/ with expand state kept on the template drawer. Co-authored-by: Cursor --- .../pages/EvalsDashboard/ProjectDatasets.tsx | 742 +----------------- .../ProjectDatasets/DatasetPreviewDrawer.tsx | 209 +++++ .../ProjectDatasets/TemplatePreviewDrawer.tsx | 602 ++++++++++++++ 3 files changed, 834 insertions(+), 719 deletions(-) create mode 100644 Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/DatasetPreviewDrawer.tsx create mode 100644 Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/TemplatePreviewDrawer.tsx diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx index dc5f8ca13c..7badb50500 100644 --- a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx @@ -1,4 +1,4 @@ -import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Box, Typography, @@ -25,7 +25,6 @@ import { ArrowLeft, Save as SaveIcon, Copy, - Database, Plus, User, Bot, @@ -71,6 +70,8 @@ import TipBox from "../../components/TipBox"; import { useAuth } from "../../../application/hooks/useAuth"; import UploadDatasetModal from "./ProjectDatasets/UploadDatasetModal"; import CreateDatasetModals from "./ProjectDatasets/CreateDatasetModals"; +import DatasetPreviewDrawer from "./ProjectDatasets/DatasetPreviewDrawer"; +import TemplatePreviewDrawer from "./ProjectDatasets/TemplatePreviewDrawer"; import type { ExampleTurnType, ExampleUseCase } from "./ProjectDatasets/exampleDatasetPayloads"; import allowedRoles from "../../../application/constants/permissions"; @@ -142,9 +143,6 @@ export function ProjectDatasets({ projectId, orgId }: ProjectDatasetsProps) { const [copyModalOpen, setCopyModalOpen] = useState(false); const [templateToCopy, setTemplateToCopy] = useState(null); - // Expanded prompt rows in template preview - const [expandedPromptIds, setExpandedPromptIds] = useState>(new Set()); - // Template drawer state const [templateDrawerOpen, setTemplateDrawerOpen] = useState(false); @@ -304,7 +302,6 @@ export function ProjectDatasets({ projectId, orgId }: ProjectDatasetsProps) { const handleViewTemplate = (template: BuiltInDataset) => { setSelectedTemplate(template); setTemplateDrawerOpen(true); - setExpandedPromptIds(new Set()); // Reset expanded state }; // Close template drawer @@ -312,7 +309,6 @@ export function ProjectDatasets({ projectId, orgId }: ProjectDatasetsProps) { setTemplateDrawerOpen(false); setSelectedTemplate(null); setTemplatePrompts([]); - setExpandedPromptIds(new Set()); // Reset expanded state }; // Load both datasets on mount so tab counts are always available @@ -1896,719 +1892,27 @@ export function ProjectDatasets({ projectId, orgId }: ProjectDatasetsProps) { onUploadClick={handleFileSelect} /> - {/* Dataset Content Drawer */} - - - {/* Header */} - - - - - {selectedDataset?.name || "Dataset"} - - {datasetPrompts.length > 0 && ( - - )} - - - - - - - - {/* Loading State */} - {loadingPrompts && ( - - - - )} - - {/* Empty State */} - {!loadingPrompts && datasetPrompts.length === 0 && ( - - - No prompts found in this dataset. - - - )} - - {/* Dataset Prompts Table */} - {!loadingPrompts && datasetPrompts.length > 0 && ( - - - - - - ID - - - {isMultiTurnConversation(datasetPrompts[0]) ? "Turns" : "Category"} - - - {isMultiTurnConversation(datasetPrompts[0]) ? "Scenario" : "Prompt"} - - - {isMultiTurnConversation(datasetPrompts[0]) ? "Outcome" : "Difficulty"} - - - - - {datasetPrompts.map((prompt: DatasetPromptRecord, index: number) => { - const isMultiTurn = isMultiTurnConversation(prompt); - return ( - - - - {prompt.id || - (isMultiTurn ? `conv_${index + 1}` : `prompt_${index + 1}`)} - - - - {isMultiTurn ? ( - - ) : ( - - )} - - - - {isMultiTurn - ? (prompt as MultiTurnConversation).scenario || - (prompt as MultiTurnConversation).turns?.[0]?.content || - "-" - : (prompt as SingleTurnPrompt).prompt || "-"} - - - - {isMultiTurn ? ( - - {(prompt as MultiTurnConversation).expected_outcome?.substring( - 0, - 20, - ) || "-"} - - ) : ( - (prompt as SingleTurnPrompt).difficulty && ( - - ) - )} - - - ); - })} - -
-
- )} -
-
- - {/* Template Content Drawer */} - - - {/* Header */} - - - - - {selectedTemplate?.name || "Template"} - - {templatePrompts.length > 0 && ( - - )} - - - - - - - - {/* Loading State */} - {loadingTemplatePrompts && ( - - - - )} - - {/* Empty State */} - {!loadingTemplatePrompts && templatePrompts.length === 0 && ( - - - - No prompts found - - - This template doesn't contain any prompts - - - )} - - {/* Prompts/Conversations Table */} - {!loadingTemplatePrompts && - templatePrompts.length > 0 && - (() => { - // Check if this is a multi-turn dataset by looking at the first item - const isMultiTurn = - templatePrompts[0] && - ("scenario" in templatePrompts[0] || "turns" in templatePrompts[0]); - - if (isMultiTurn) { - // Multi-turn dataset display - cast to any for flexible access - const conversations = templatePrompts as unknown as Array<{ - scenario?: string; - category?: string; - expected_outcome?: string; - turns?: Array<{ role: string; content: string }>; - }>; - - return ( - - - - - - # - - - Category - - - Scenario - - - Turns - - - - - {conversations.map((conversation, index) => { - const convKey = `conv-${index}`; - const isExpanded = expandedPromptIds.has(convKey); - const turns = conversation.turns || []; - const scenarioText = conversation.scenario || `Conversation ${index + 1}`; - const isLongScenario = scenarioText.length > 50; - // Try to infer category from scenario or use a default - const category = - conversation.category || - (scenarioText.toLowerCase().includes("troubleshoot") - ? "SUPPORT" - : scenarioText.toLowerCase().includes("install") - ? "SETUP" - : scenarioText.toLowerCase().includes("api") - ? "TECHNICAL" - : scenarioText.toLowerCase().includes("crash") - ? "DEBUG" - : "GENERAL"); + - return ( - - { - setExpandedPromptIds((prev) => { - const newSet = new Set(prev); - if (newSet.has(convKey)) { - newSet.delete(convKey); - } else { - newSet.add(convKey); - } - return newSet; - }); - }} - sx={{ - ...singleTheme.tableStyles.primary.body.row, - "cursor": "pointer", - "&:hover": { backgroundColor: palette.background.accent }, - "verticalAlign": "top", - }} - > - - - {index + 1} - - - - - 10 - ? `${category.substring(0, 10)}...` - : category - } - size="small" - backgroundColor={palette.border.dark} - textColor={palette.text.secondary} - /> - - - - - {isExpanded - ? scenarioText - : isLongScenario - ? `${scenarioText.substring(0, 50)}...` - : scenarioText} - - {(isLongScenario || turns.length > 0) && ( - - {isExpanded ? "Collapse" : "Expand"} - - )} - - - - - - - {/* Expanded conversation turns */} - {isExpanded && ( - - - - {conversation.expected_outcome && ( - - - Expected Outcome - - - {conversation.expected_outcome} - - - )} - - {turns.map((turn, turnIdx) => ( - - - - {turn.role === "user" ? "User" : "Assistant"} - - - {turn.content} - - - - ))} - - - - - )} - - ); - })} - -
-
- ); - } - - // Single-turn dataset display (original table) - const singleTurnPrompts = templatePrompts as SingleTurnPrompt[]; - return ( - - - - - - # - - - Category - - - Prompt - - - Difficulty - - - - - {singleTurnPrompts.map((prompt, index) => { - const promptKey = prompt.id || `prompt-${index}`; - const isExpanded = expandedPromptIds.has(promptKey); - const promptText = prompt.prompt || ""; - const isLongPrompt = promptText.length > 40; - - return ( - { - if (isLongPrompt) { - setExpandedPromptIds((prev) => { - const newSet = new Set(prev); - if (newSet.has(promptKey)) { - newSet.delete(promptKey); - } else { - newSet.add(promptKey); - } - return newSet; - }); - } - }} - sx={{ - ...singleTheme.tableStyles.primary.body.row, - "cursor": isLongPrompt ? "pointer" : "default", - "&:hover": isLongPrompt - ? { backgroundColor: palette.background.accent } - : {}, - "verticalAlign": "top", - }} - > - - - {index + 1} - - - - - 8 - ? `${prompt.category.substring(0, 8)}...` - : prompt.category || "-" - } - size="small" - backgroundColor={palette.border.dark} - textColor={palette.text.secondary} - /> - - - - - {isExpanded - ? promptText - : isLongPrompt - ? `${promptText.substring(0, 40)}...` - : promptText} - - {isLongPrompt && ( - - {isExpanded ? "Collapse" : "Expand"} - - )} - - - {prompt.difficulty && ( - - )} - - - ); - })} - -
-
- ); - })()} - - {/* Copy Button */} - {!loadingTemplatePrompts && templatePrompts.length > 0 && selectedTemplate && ( - } - onClick={() => handleOpenCopyModal(selectedTemplate)} - isDisabled={copyingTemplate} - text={copyingTemplate ? "Copying..." : "Copy to my datasets"} - sx={{ - mt: 4, - minHeight: "40px", - }} - /> - )} -
-
+ { + if (selectedTemplate) { + handleOpenCopyModal(selectedTemplate); + } + }} + /> void; + datasetName?: string; + prompts: DatasetPromptRecord[]; + loading: boolean; +}; + +export default function DatasetPreviewDrawer({ + open, + onClose, + datasetName, + prompts, + loading, +}: DatasetPreviewDrawerProps) { + const theme = useTheme(); + + return ( + + + {/* Header */} + + + + + {datasetName || "Dataset"} + + {prompts.length > 0 && ( + + )} + + + + + + + + {/* Loading State */} + {loading && ( + + + + )} + + {/* Empty State */} + {!loading && prompts.length === 0 && ( + + + No prompts found in this dataset. + + + )} + + {/* Dataset Prompts Table */} + {!loading && prompts.length > 0 && ( + + + + + + ID + + + {isMultiTurnConversation(prompts[0]) ? "Turns" : "Category"} + + + {isMultiTurnConversation(prompts[0]) ? "Scenario" : "Prompt"} + + + {isMultiTurnConversation(prompts[0]) ? "Outcome" : "Difficulty"} + + + + + {prompts.map((prompt: DatasetPromptRecord, index: number) => { + const isMultiTurn = isMultiTurnConversation(prompt); + return ( + + + + {prompt.id || (isMultiTurn ? `conv_${index + 1}` : `prompt_${index + 1}`)} + + + + {isMultiTurn ? ( + + ) : ( + + )} + + + + {isMultiTurn + ? (prompt as MultiTurnConversation).scenario || + (prompt as MultiTurnConversation).turns?.[0]?.content || + "-" + : (prompt as SingleTurnPrompt).prompt || "-"} + + + + {isMultiTurn ? ( + + {(prompt as MultiTurnConversation).expected_outcome?.substring(0, 20) || + "-"} + + ) : ( + (prompt as SingleTurnPrompt).difficulty && ( + + ) + )} + + + ); + })} + +
+
+ )} +
+
+ ); +} diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/TemplatePreviewDrawer.tsx b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/TemplatePreviewDrawer.tsx new file mode 100644 index 0000000000..5956cee0b7 --- /dev/null +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/TemplatePreviewDrawer.tsx @@ -0,0 +1,602 @@ +/** + * @fileoverview Read-only preview drawer for a built-in template dataset, + * including expand/collapse and copy-to-my-datasets CTA. + * + * @module pages/EvalsDashboard/ProjectDatasets/TemplatePreviewDrawer + */ + +import { Fragment, useEffect, useState } from "react"; +import { + Box, + CircularProgress, + Divider, + Drawer, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + useTheme, +} from "@mui/material"; +import { Copy, Database, X } from "lucide-react"; +import { CustomizableButton } from "../../../components/button/customizable-button"; +import Chip from "../../../components/Chip"; +import singleTheme from "../../../themes/v1SingleTheme"; +import { palette } from "../../../themes/palette"; +import type { + DatasetPromptRecord, + SingleTurnPrompt, +} from "../../../../application/repository/deepEval.repository"; + +export type TemplatePreviewDrawerProps = { + open: boolean; + onClose: () => void; + templateName?: string; + prompts: DatasetPromptRecord[]; + loading: boolean; + copying?: boolean; + onCopy?: () => void; +}; + +export default function TemplatePreviewDrawer({ + open, + onClose, + templateName, + prompts, + loading, + copying = false, + onCopy, +}: TemplatePreviewDrawerProps) { + const theme = useTheme(); + const [expandedPromptIds, setExpandedPromptIds] = useState>(new Set()); + + useEffect(() => { + if (!open) { + setExpandedPromptIds(new Set()); + } + }, [open]); + + useEffect(() => { + setExpandedPromptIds(new Set()); + }, [templateName]); + + return ( + + + {/* Header */} + + + + + {templateName || "Template"} + + {prompts.length > 0 && ( + + )} + + + + + + + + {/* Loading State */} + {loading && ( + + + + )} + + {/* Empty State */} + {!loading && prompts.length === 0 && ( + + + + No prompts found + + + This template doesn't contain any prompts + + + )} + + {/* Prompts/Conversations Table */} + {!loading && + prompts.length > 0 && + (() => { + // Check if this is a multi-turn dataset by looking at the first item + const isMultiTurn = prompts[0] && ("scenario" in prompts[0] || "turns" in prompts[0]); + + if (isMultiTurn) { + // Multi-turn dataset display - cast to any for flexible access + const conversations = prompts as unknown as Array<{ + scenario?: string; + category?: string; + expected_outcome?: string; + turns?: Array<{ role: string; content: string }>; + }>; + + return ( + + + + + + # + + + Category + + + Scenario + + + Turns + + + + + {conversations.map((conversation, index) => { + const convKey = `conv-${index}`; + const isExpanded = expandedPromptIds.has(convKey); + const turns = conversation.turns || []; + const scenarioText = conversation.scenario || `Conversation ${index + 1}`; + const isLongScenario = scenarioText.length > 50; + // Try to infer category from scenario or use a default + const category = + conversation.category || + (scenarioText.toLowerCase().includes("troubleshoot") + ? "SUPPORT" + : scenarioText.toLowerCase().includes("install") + ? "SETUP" + : scenarioText.toLowerCase().includes("api") + ? "TECHNICAL" + : scenarioText.toLowerCase().includes("crash") + ? "DEBUG" + : "GENERAL"); + + return ( + + { + setExpandedPromptIds((prev) => { + const newSet = new Set(prev); + if (newSet.has(convKey)) { + newSet.delete(convKey); + } else { + newSet.add(convKey); + } + return newSet; + }); + }} + sx={{ + ...singleTheme.tableStyles.primary.body.row, + "cursor": "pointer", + "&:hover": { backgroundColor: palette.background.accent }, + "verticalAlign": "top", + }} + > + + + {index + 1} + + + + + 10 + ? `${category.substring(0, 10)}...` + : category + } + size="small" + backgroundColor={palette.border.dark} + textColor={palette.text.secondary} + /> + + + + + {isExpanded + ? scenarioText + : isLongScenario + ? `${scenarioText.substring(0, 50)}...` + : scenarioText} + + {(isLongScenario || turns.length > 0) && ( + + {isExpanded ? "Collapse" : "Expand"} + + )} + + + + + + + {/* Expanded conversation turns */} + {isExpanded && ( + + + + {conversation.expected_outcome && ( + + + Expected Outcome + + + {conversation.expected_outcome} + + + )} + + {turns.map((turn, turnIdx) => ( + + + + {turn.role === "user" ? "User" : "Assistant"} + + + {turn.content} + + + + ))} + + + + + )} + + ); + })} + +
+
+ ); + } + + // Single-turn dataset display (original table) + const singleTurnPrompts = prompts as SingleTurnPrompt[]; + return ( + + + + + + # + + + Category + + + Prompt + + + Difficulty + + + + + {singleTurnPrompts.map((prompt, index) => { + const promptKey = prompt.id || `prompt-${index}`; + const isExpanded = expandedPromptIds.has(promptKey); + const promptText = prompt.prompt || ""; + const isLongPrompt = promptText.length > 40; + + return ( + { + if (isLongPrompt) { + setExpandedPromptIds((prev) => { + const newSet = new Set(prev); + if (newSet.has(promptKey)) { + newSet.delete(promptKey); + } else { + newSet.add(promptKey); + } + return newSet; + }); + } + }} + sx={{ + ...singleTheme.tableStyles.primary.body.row, + "cursor": isLongPrompt ? "pointer" : "default", + "&:hover": isLongPrompt + ? { backgroundColor: palette.background.accent } + : {}, + "verticalAlign": "top", + }} + > + + + {index + 1} + + + + + 8 + ? `${prompt.category.substring(0, 8)}...` + : prompt.category || "-" + } + size="small" + backgroundColor={palette.border.dark} + textColor={palette.text.secondary} + /> + + + + + {isExpanded + ? promptText + : isLongPrompt + ? `${promptText.substring(0, 40)}...` + : promptText} + + {isLongPrompt && ( + + {isExpanded ? "Collapse" : "Expand"} + + )} + + + {prompt.difficulty && ( + + )} + + + ); + })} + +
+
+ ); + })()} + + {/* Copy Button */} + {!loading && prompts.length > 0 && templateName && ( + } + onClick={onCopy} + isDisabled={copying} + text={copying ? "Copying..." : "Copy to my datasets"} + sx={{ + mt: 4, + minHeight: "40px", + }} + /> + )} +
+
+ ); +} From b189116ba9594f5430550da613a53ef3880dc5c7 Mon Sep 17 00:00:00 2001 From: Inna Glamazda Date: Fri, 7 Aug 2026 11:53:53 -0400 Subject: [PATCH 5/8] refactor(evals): extract editor, tabs, and useProjectDatasets hook Move DatasetInlineEditor, PromptEditDrawer, MyDatasetsTab, TemplatesTab, shared types, and dataset loaders/filters/CRUD into ProjectDatasets/, leaving the parent as composition. Co-authored-by: Cursor --- .../pages/EvalsDashboard/ProjectDatasets.tsx | 1939 ++--------------- .../ProjectDatasets/DatasetInlineEditor.tsx | 446 ++++ .../ProjectDatasets/MyDatasetsTab.tsx | 182 ++ .../ProjectDatasets/PromptEditDrawer.tsx | 468 ++++ .../ProjectDatasets/TemplatesTab.tsx | 99 + .../EvalsDashboard/ProjectDatasets/types.ts | 29 + .../ProjectDatasets/useProjectDatasets.ts | 704 ++++++ 7 files changed, 2053 insertions(+), 1814 deletions(-) create mode 100644 Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/DatasetInlineEditor.tsx create mode 100644 Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/MyDatasetsTab.tsx create mode 100644 Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/PromptEditDrawer.tsx create mode 100644 Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/TemplatesTab.tsx create mode 100644 Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/types.ts create mode 100644 Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/useProjectDatasets.ts diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx index 7badb50500..aa17550601 100644 --- a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets.tsx @@ -1,680 +1,35 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { - Box, - Typography, - Stack, - Drawer, - Divider, - CircularProgress, - TableContainer, - Table, - TableHead, - TableRow, - TableCell, - TableBody, - useTheme, - IconButton, - Menu, -} from "@mui/material"; -import { - Upload, - Download, - X, - Edit3, - Trash2, - ArrowLeft, - Save as SaveIcon, - Copy, - Plus, - User, - Bot, - Check, - Eye, -} from "lucide-react"; -import { CustomizableButton } from "../../components/button/customizable-button"; +/** + * @fileoverview Project Datasets page: my datasets and built-in templates for LLM evals. + * + * @module pages/EvalsDashboard/ProjectDatasets + */ + +import { Box, CircularProgress, Stack, Typography } from "@mui/material"; import TabContext from "@mui/lab/TabContext"; import TabBar from "../../components/TabBar"; -import { - listMyDatasets, - listDatasets, - readDataset, - uploadDataset, - deleteDatasets, - type DatasetPromptRecord, - type ListedDataset, - type DatasetType, -} from "../../../application/repository/deepEval.repository"; -import { - isSingleTurnPrompt, - isMultiTurnConversation, - type SingleTurnPrompt, - type MultiTurnConversation, -} from "../../../application/repository/deepEval.repository"; import Alert from "../../components/Alert"; -import Chip from "../../components/Chip"; import ConfirmationModal from "../../components/Dialogs/ConfirmationModal"; -import Field from "../../components/Inputs/Field"; -import SearchBox from "../../components/Search/SearchBox"; -import { FilterBy, type FilterColumn } from "../../components/Table/FilterBy"; -import { GroupBy } from "../../components/Table/GroupBy"; -import { GroupedTableView } from "../../components/Table/GroupedTableView"; -import { useTableGrouping, useGroupByState } from "../../../application/hooks/useTableGrouping"; -import { useFilterBy } from "../../../application/hooks/useFilterBy"; -import singleTheme from "../../themes/v1SingleTheme"; -import { palette } from "../../themes/palette"; -import DatasetsTable, { type DatasetRow } from "../../components/Table/DatasetsTable"; -import TemplatesTable from "../../components/Table/TemplatesTable"; import { PageHeader } from "../../components/Layout/PageHeader"; import HelperIcon from "../../components/HelperIcon"; import TipBox from "../../components/TipBox"; -import { useAuth } from "../../../application/hooks/useAuth"; +import { palette } from "../../themes/palette"; import UploadDatasetModal from "./ProjectDatasets/UploadDatasetModal"; import CreateDatasetModals from "./ProjectDatasets/CreateDatasetModals"; import DatasetPreviewDrawer from "./ProjectDatasets/DatasetPreviewDrawer"; import TemplatePreviewDrawer from "./ProjectDatasets/TemplatePreviewDrawer"; -import type { ExampleTurnType, ExampleUseCase } from "./ProjectDatasets/exampleDatasetPayloads"; -import allowedRoles from "../../../application/constants/permissions"; +import DatasetInlineEditor from "./ProjectDatasets/DatasetInlineEditor"; +import MyDatasetsTab from "./ProjectDatasets/MyDatasetsTab"; +import TemplatesTab from "./ProjectDatasets/TemplatesTab"; +import { useProjectDatasets } from "./ProjectDatasets/useProjectDatasets"; type ProjectDatasetsProps = { projectId: string; orgId?: string | null }; -type BuiltInDataset = ListedDataset & { - promptCount?: number; - isUserDataset?: boolean; - createdAt?: string; - datasetType?: DatasetType; - turnType?: "single-turn" | "multi-turn" | "simulated"; - // Additional metadata for templates - test_count?: number; - categories?: string[]; - category_count?: number; - difficulty?: { easy: number; medium: number; hard: number }; - description?: string; - tags?: string[]; -}; - export function ProjectDatasets({ projectId, orgId }: ProjectDatasetsProps) { void projectId; // Used for future project-scoped features - const theme = useTheme(); - - // RBAC permissions - const { userRoleName, isSuperAdmin } = useAuth(); - const canUploadDataset = allowedRoles.evals.uploadDataset.includes(userRoleName) && !isSuperAdmin; - const canDeleteDataset = allowedRoles.evals.deleteDataset.includes(userRoleName) && !isSuperAdmin; - - // Tab state: "my" for user datasets, "templates" for built-in datasets - const [activeTab, setActiveTab] = useState<"my" | "templates">("my"); - - // My datasets state - const [datasets, setDatasets] = useState([]); - const [loading, setLoading] = useState(false); - const [loadingTemplatesList, setLoadingTemplatesList] = useState(false); - const [searchTerm, setSearchTerm] = useState(""); - const { - groupBy: datasetsGroupBy, - groupSortOrder: datasetsGroupSortOrder, - handleGroupChange: handleDatasetsGroupChange, - } = useGroupByState(); - const [alert, setAlert] = useState<{ variant: "success" | "error"; body: string } | null>(null); - const [uploading, setUploading] = useState(false); - const [uploadModalOpen, setUploadModalOpen] = useState(false); - const fileInputRef = useRef(null); - const [drawerOpen, setDrawerOpen] = useState(false); - const [selectedDataset, setSelectedDataset] = useState(null); - const [datasetPrompts, setDatasetPrompts] = useState([]); - const [loadingPrompts, setLoadingPrompts] = useState(false); - - // Template datasets state - const [templateGroups, setTemplateGroups] = useState< - Record<"chatbot" | "rag" | "agent", BuiltInDataset[]> - >({ - chatbot: [], - rag: [], - agent: [], - }); - const [selectedTemplate, setSelectedTemplate] = useState(null); - const [templatePrompts, setTemplatePrompts] = useState([]); - const [loadingTemplatePrompts, setLoadingTemplatePrompts] = useState(false); - const [copyingTemplate, setCopyingTemplate] = useState(false); - - // Template table state (search) - sorting and pagination handled by TemplatesTable component - const [templateSearchTerm, setTemplateSearchTerm] = useState(""); - - // Copy template confirmation modal state - const [copyModalOpen, setCopyModalOpen] = useState(false); - const [templateToCopy, setTemplateToCopy] = useState(null); - - // Template drawer state - const [templateDrawerOpen, setTemplateDrawerOpen] = useState(false); - - // Action menu state - const [actionAnchor, setActionAnchor] = useState(null); - const [actionDataset, setActionDataset] = useState(null); - - // Delete modal state - const [deleteModalOpen, setDeleteModalOpen] = useState(false); - const [datasetToDelete, setDatasetToDelete] = useState(null); - - // Inline editor state - const [editorOpen, setEditorOpen] = useState(false); - const [editingDataset, setEditingDataset] = useState(null); - const [editablePrompts, setEditablePrompts] = useState([]); - const [editDatasetName, setEditDatasetName] = useState(""); - const [savingDataset, setSavingDataset] = useState(false); - const [loadingEditor, setLoadingEditor] = useState(false); - const [copiedJson, setCopiedJson] = useState(false); - - // Prompt edit drawer state (for inline editor) - const [promptDrawerOpen, setPromptDrawerOpen] = useState(false); - const [selectedPromptIndex, setSelectedPromptIndex] = useState(null); - - // Create dataset modal state - const [createDatasetModalOpen, setCreateDatasetModalOpen] = useState(false); - const [createTypeSelectionOpen, setCreateTypeSelectionOpen] = useState(false); - - // Note: promptCount is now returned by the API - no need to load metadata individually - - // Load user's datasets (My datasets tab) - const loadMyDatasets = useCallback(async () => { - try { - setLoading(true); - const userRes = await listMyDatasets().catch(() => ({ datasets: [] })); - const userDatasets = userRes.datasets || []; - const allDatasets: BuiltInDataset[] = userDatasets.map((ud) => ({ - key: `user_${ud.id}`, - name: ud.name, - path: ud.path, - use_case: (ud.datasetType || "chatbot") as "chatbot" | "rag" | "agent", - datasetType: ud.datasetType || "chatbot", - turnType: ud.turnType, - isUserDataset: true, - createdAt: ud.createdAt, - promptCount: ud.promptCount || 0, // Use pre-computed count from API - })); - setDatasets(allDatasets); - } catch (err) { - console.error("Failed to load datasets", err); - setDatasets([]); - setAlert({ - variant: "error", - body: "Failed to load datasets", - }); - setTimeout(() => setAlert(null), 5000); - } finally { - setLoading(false); - } - }, []); - - // Load built-in template datasets (Templates tab) - const loadTemplateDatasets = useCallback(async () => { - try { - setLoadingTemplatesList(true); - const res = await listDatasets(); - setTemplateGroups(res as Record<"chatbot" | "rag" | "agent", BuiltInDataset[]>); - } catch (err) { - console.error("Failed to load template datasets", err); - setTemplateGroups({ chatbot: [], rag: [], agent: [] }); - setAlert({ - variant: "error", - body: "Failed to load template datasets", - }); - setTimeout(() => setAlert(null), 5000); - } finally { - setLoadingTemplatesList(false); - } - }, []); - - // Flatten templates from all categories into a single array with category field - type TemplateWithCategory = BuiltInDataset & { category: "chatbot" | "rag" | "agent" }; - const flattenedTemplates: TemplateWithCategory[] = useMemo(() => { - return (["chatbot", "rag", "agent"] as const).flatMap((category) => - (templateGroups[category] || []).map((ds) => ({ ...ds, category })), - ); - }, [templateGroups]); - - // Template filter columns - const templateFilterColumns: FilterColumn[] = useMemo( - () => [ - { id: "name", label: "Dataset name", type: "text" }, - { - id: "category", - label: "Category", - type: "select", - options: [ - { value: "chatbot", label: "Chatbot" }, - { value: "rag", label: "RAG" }, - { value: "agent", label: "Agent" }, - ], - }, - ], - [], - ); - - // Template field value getter for filtering - const getTemplateFieldValue = useCallback( - (item: TemplateWithCategory, fieldId: string): string | number | Date | null | undefined => { - switch (fieldId) { - case "name": - return item.name; - case "category": - return item.category; - default: - return null; - } - }, - [], - ); - - // useFilterBy hook for templates - const { filterData: filterTemplateData, handleFilterChange: handleTemplateFilterChange } = - useFilterBy(getTemplateFieldValue); - - // Filtered templates (sorting and pagination handled by TemplatesTable component) - const filteredAndSortedTemplates = useMemo(() => { - // Apply filter - let result = filterTemplateData(flattenedTemplates); - - // Apply search - if (templateSearchTerm.trim()) { - const q = templateSearchTerm.toLowerCase(); - result = result.filter( - (t) => t.name.toLowerCase().includes(q) || t.category.toLowerCase().includes(q), - ); - } - - return result; - }, [flattenedTemplates, filterTemplateData, templateSearchTerm]); - - // Open copy confirmation modal - const handleOpenCopyModal = (template: BuiltInDataset) => { - setTemplateToCopy(template); - setCopyModalOpen(true); - }; - - // Confirm copy - const handleConfirmCopy = async () => { - if (!templateToCopy) return; - setCopyModalOpen(false); - await handleCopyTemplate(templateToCopy); - setTemplateToCopy(null); - }; - - // Open template drawer - const handleViewTemplate = (template: BuiltInDataset) => { - setSelectedTemplate(template); - setTemplateDrawerOpen(true); - }; - - // Close template drawer - const handleCloseTemplateDrawer = () => { - setTemplateDrawerOpen(false); - setSelectedTemplate(null); - setTemplatePrompts([]); - }; - - // Load both datasets on mount so tab counts are always available - useEffect(() => { - void loadMyDatasets(); - void loadTemplateDatasets(); - }, [loadMyDatasets, loadTemplateDatasets]); - - // Load template prompts when a template is selected - useEffect(() => { - if (!selectedTemplate?.path) { - setTemplatePrompts([]); - return; - } - (async () => { - try { - setLoadingTemplatePrompts(true); - const res = await readDataset(selectedTemplate.path); - setTemplatePrompts(res.prompts || []); - } catch (err) { - console.error("Failed to load template prompts", err); - setTemplatePrompts([]); - } finally { - setLoadingTemplatePrompts(false); - } - })(); - }, [selectedTemplate]); - - // Copy template to user's datasets - const handleCopyTemplate = async (template: BuiltInDataset) => { - try { - setCopyingTemplate(true); - // Load the template content - const res = await readDataset(template.path); - const prompts = res.prompts || []; - - // Create a new file and upload it - const json = JSON.stringify(prompts, null, 2); - const blob = new Blob([json], { type: "application/json" }); - const fileName = `${template.name.toLowerCase().replace(/[^a-z0-9]+/g, "_")}.json`; - const file = new File([blob], fileName, { type: "application/json" }); - - await uploadDataset(file, "chatbot", "single-turn", orgId || undefined); - setAlert({ variant: "success", body: `"${template.name}" copied to your datasets` }); - setTimeout(() => setAlert(null), 3000); - - // Switch to My datasets tab and reload - setActiveTab("my"); - void loadMyDatasets(); - } catch (err) { - console.error("Failed to copy template", err); - setAlert({ variant: "error", body: "Failed to copy template" }); - setTimeout(() => setAlert(null), 5000); - } finally { - setCopyingTemplate(false); - } - }; - - const filterColumns: FilterColumn[] = useMemo( - () => [{ id: "name", label: "Dataset name", type: "text" }], - [], - ); - - const getFieldValue = useCallback( - (d: BuiltInDataset, fieldId: string): string | number | Date | null | undefined => { - switch (fieldId) { - case "name": - return d.name; - case "use_case": - return d.use_case; - default: - return ""; - } - }, - [], - ); - - const { filterData, handleFilterChange } = useFilterBy(getFieldValue); - - const filteredDatasets = useMemo(() => { - const afterFilter = filterData(datasets); - if (!searchTerm.trim()) return afterFilter; - const q = searchTerm.toLowerCase(); - return afterFilter.filter((d) => - [d.name, d.path, d.use_case].filter(Boolean).join(" ").toLowerCase().includes(q), - ); - }, [datasets, filterData, searchTerm]); - - // Datasets grouping - const getDatasetGroupKey = useCallback((dataset: BuiltInDataset, field: string): string => { - switch (field) { - case "name": - // Group by first letter - return dataset.name?.charAt(0).toUpperCase() || "Other"; - case "prompts": { - const count = dataset.promptCount ?? 0; - if (count === 0) return "No prompts"; - if (count <= 10) return "1-10 prompts"; - if (count <= 50) return "11-50 prompts"; - if (count <= 100) return "51-100 prompts"; - return "100+ prompts"; - } - case "createdAt": { - if (!dataset.createdAt) return "Unknown"; - const date = new Date(dataset.createdAt); - const now = new Date(); - const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24)); - if (diffDays === 0) return "Today"; - if (diffDays === 1) return "Yesterday"; - if (diffDays <= 7) return "This week"; - if (diffDays <= 30) return "This month"; - return "Older"; - } - default: - return "Other"; - } - }, []); - - const groupedDatasets = useTableGrouping({ - data: filteredDatasets, - groupByField: datasetsGroupBy, - sortOrder: datasetsGroupSortOrder, - getGroupKey: getDatasetGroupKey, - }); - - // Action menu handlers - const handleActionMenuClose = () => { - setActionAnchor(null); - setActionDataset(null); - }; - - const handleViewPrompts = async (dataset: BuiltInDataset) => { - handleActionMenuClose(); - setSelectedDataset(dataset); - setDrawerOpen(true); - try { - setLoadingPrompts(true); - const res = await readDataset(dataset.path); - setDatasetPrompts(res.prompts || []); - } catch (err) { - console.error("Failed to load dataset prompts", err); - setDatasetPrompts([]); - } finally { - setLoadingPrompts(false); - } - }; - - const handleOpenInEditor = async (dataset: BuiltInDataset) => { - handleActionMenuClose(); - try { - setLoadingEditor(true); - const res = await readDataset(dataset.path); - setEditablePrompts(res.prompts || []); - // Use the dataset name directly (already cleaned by backend) - setEditDatasetName(dataset.name); - setEditingDataset(dataset); - setEditorOpen(true); - } catch (err) { - console.error("Failed to load dataset for editing", err); - setAlert({ variant: "error", body: "Failed to load dataset for editing" }); - setTimeout(() => setAlert(null), 5000); - } finally { - setLoadingEditor(false); - } - }; - - const handleCloseEditor = () => { - setEditorOpen(false); - setEditingDataset(null); - setEditablePrompts([]); - setEditDatasetName(""); - }; - - const handleSaveDataset = async () => { - try { - setSavingDataset(true); - const json = JSON.stringify(editablePrompts, null, 2); - const blob = new Blob([json], { type: "application/json" }); - const slug = editDatasetName - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, ""); - const finalName = slug ? `${slug}.json` : "dataset.json"; - const file = new File([blob], finalName, { type: "application/json" }); - const datasetType = editingDataset?.datasetType || "chatbot"; - - // If editing an existing user dataset, delete the old one first - if (editingDataset?.isUserDataset && editingDataset?.path) { - try { - await deleteDatasets([editingDataset.path]); - } catch (deleteErr) { - console.warn("Could not delete old dataset, proceeding with save:", deleteErr); - } - } - - const turnType = - editablePrompts.length > 0 && !isSingleTurnPrompt(editablePrompts[0]) - ? "multi-turn" - : "single-turn"; - await uploadDataset(file, datasetType, turnType, orgId || undefined); - setAlert({ variant: "success", body: `Dataset "${editDatasetName}" saved successfully!` }); - setTimeout(() => setAlert(null), 3000); - handleCloseEditor(); - void loadMyDatasets(); - } catch (err) { - console.error("Failed to save dataset", err); - type AxiosLike = { response?: { data?: unknown } }; - const axiosErr = err as AxiosLike | Error; - const resData = (axiosErr as AxiosLike)?.response?.data as - | Record - | undefined; - const serverMsg = - (resData && (String(resData.message ?? "") || String(resData.detail ?? ""))) || - (axiosErr instanceof Error ? axiosErr.message : null); - setAlert({ variant: "error", body: serverMsg || "Save failed. Check dataset structure." }); - setTimeout(() => setAlert(null), 6000); - } finally { - setSavingDataset(false); - } - }; - - const isValidToSave = useMemo(() => { - if (!editablePrompts || editablePrompts.length === 0 || !editDatasetName.trim()) return false; - // Check if any record has valid content (single-turn prompt or multi-turn turns) - return editablePrompts.some((p) => { - if (isSingleTurnPrompt(p)) { - return p.prompt.trim().length > 0; - } else if (isMultiTurnConversation(p)) { - return p.turns && p.turns.length > 0 && p.turns.some((t) => t.content.trim().length > 0); - } - return false; - }); - }, [editablePrompts, editDatasetName]); - - const handleAddPrompt = () => { - const newPrompt: DatasetPromptRecord = { - id: `prompt_${Date.now()}`, - category: "General", - prompt: "", - expected_output: "", - expected_keywords: [], - retrieval_context: [], - }; - setEditablePrompts((prev) => [...prev, newPrompt]); - // Open the drawer with the new prompt - setSelectedPromptIndex(editablePrompts.length); - setPromptDrawerOpen(true); - }; - - const handleDeletePrompt = (idx: number) => { - setEditablePrompts((prev) => prev.filter((_, i) => i !== idx)); - if (selectedPromptIndex === idx) { - setPromptDrawerOpen(false); - setSelectedPromptIndex(null); - } else if (selectedPromptIndex !== null && selectedPromptIndex > idx) { - setSelectedPromptIndex(selectedPromptIndex - 1); - } - }; - - const handleRemoveDataset = (dataset: BuiltInDataset) => { - handleActionMenuClose(); - setDatasetToDelete(dataset); - setDeleteModalOpen(true); - }; - - const handleConfirmDelete = async () => { - if (!datasetToDelete) return; - try { - await deleteDatasets([datasetToDelete.path]); - setAlert({ variant: "success", body: "Dataset removed" }); - setTimeout(() => setAlert(null), 3000); - void loadMyDatasets(); - } catch (err) { - console.error("Failed to remove dataset", err); - setAlert({ variant: "error", body: "Failed to remove dataset" }); - setTimeout(() => setAlert(null), 5000); - } finally { - setDeleteModalOpen(false); - setDatasetToDelete(null); - } - }; - - const handleRowClick = async (dataset: BuiltInDataset) => { - // Navigate directly to editor when clicking on a row - await handleOpenInEditor(dataset); - }; - - const handleDownloadDataset = async (dataset: BuiltInDataset) => { - handleActionMenuClose(); - try { - const res = await readDataset(dataset.path); - const json = JSON.stringify(res.prompts || [], null, 2); - const blob = new Blob([json], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - const slug = - dataset.name - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, "") || "dataset"; - a.download = `${slug}.json`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } catch (err) { - console.error("Failed to download dataset", err); - setAlert({ variant: "error", body: "Failed to download dataset" }); - setTimeout(() => setAlert(null), 5000); - } - }; - - // Metadata is now pre-computed by backend - no need to load individually - - const handleCloseDrawer = () => { - setDrawerOpen(false); - setSelectedDataset(null); - setDatasetPrompts([]); - }; - - const handleUploadClick = () => { - setUploadModalOpen(true); - }; - - const handleFileSelect = () => { - fileInputRef.current?.click(); - }; - - // Example dataset type for download / upload metadata - const [exampleDatasetType, setExampleDatasetType] = useState("chatbot"); - const [datasetTurnType, setDatasetTurnType] = useState("single-turn"); - - const handleFileChange = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - - try { - setUploading(true); - setUploadModalOpen(false); - const resp = await uploadDataset( - file, - exampleDatasetType, - datasetTurnType, - orgId || undefined, - ); - setAlert({ variant: "success", body: `Uploaded ${resp.filename}` }); - setTimeout(() => setAlert(null), 4000); - void loadMyDatasets(); - } catch (err) { - console.error("Upload failed", err); - setAlert({ - variant: "error", - body: err instanceof Error ? err.message : "Upload failed", - }); - setTimeout(() => setAlert(null), 6000); - } finally { - setUploading(false); - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } - } - }; + const ds = useProjectDatasets({ orgId }); // If editor is loading, show spinner - if (loadingEditor) { + if (ds.loadingEditor) { return ( - {alert && } - - {/* Header with back button and save */} - - - - - - - Edit dataset - - - - { - try { - const json = JSON.stringify(editablePrompts, null, 2); - await navigator.clipboard.writeText(json); - setCopiedJson(true); - setTimeout(() => setCopiedJson(false), 2000); - } catch { - setAlert({ variant: "error", body: "Failed to copy to clipboard" }); - setTimeout(() => setAlert(null), 3000); - } - }} - startIcon={copiedJson ? : } - text={copiedJson ? "Copied!" : "Copy JSON"} - sx={{ - "color": copiedJson ? palette.status.success.text : palette.text.secondary, - "borderColor": copiedJson ? palette.status.success.text : palette.border.dark, - "&:hover": { - borderColor: palette.text.disabled, - backgroundColor: palette.background.accent, - }, - }} - /> - { - const json = JSON.stringify(editablePrompts, null, 2); - const blob = new Blob([json], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - const slug = - editDatasetName - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, "") || "dataset"; - a.download = `${slug}.json`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }} - startIcon={} - text="Download" - sx={{ - "color": palette.text.secondary, - "borderColor": palette.border.dark, - "&:hover": { - borderColor: palette.text.disabled, - backgroundColor: palette.background.accent, - }, - }} - /> - } - onClick={handleSaveDataset} - text={savingDataset ? "Saving..." : "Save"} - /> - - - - {/* Dataset name input */} - - setEditDatasetName(e.target.value)} - placeholder="Enter a descriptive name for this dataset" - isRequired - /> - - Edit the prompts below, then click Save to update your dataset. - - - - {/* Prompts/Conversations table */} - - - - - - ID - - - {editablePrompts.length > 0 && isMultiTurnConversation(editablePrompts[0]) - ? "SCENARIO / TURNS" - : "PROMPT"} - - - {editablePrompts.length > 0 && isMultiTurnConversation(editablePrompts[0]) - ? "TURNS" - : "DIFFICULTY"} - - - {editablePrompts.length > 0 && isMultiTurnConversation(editablePrompts[0]) - ? "OUTCOME" - : "CATEGORY"} - - - - - - {editablePrompts.length === 0 ? ( - - - - No prompts in this dataset yet. - - } - onClick={handleAddPrompt} - text="Add your first prompt" - sx={{ - "color": palette.brand.primary, - "borderColor": palette.brand.primary, - "&:hover": { - borderColor: palette.brand.primaryHover, - backgroundColor: palette.brand.primaryLight, - }, - }} - /> - - - ) : ( - editablePrompts.map((p, idx) => { - const isMultiTurn = isMultiTurnConversation(p); - const displayText = isMultiTurn - ? (p as MultiTurnConversation).scenario || - (p as MultiTurnConversation).turns?.[0]?.content || - "Empty conversation" - : (p as SingleTurnPrompt).prompt || "Empty prompt - click to edit"; - const hasContent = isMultiTurn - ? (p as MultiTurnConversation).turns?.length > 0 - : !!(p as SingleTurnPrompt).prompt; - - return ( - { - setSelectedPromptIndex(idx); - setPromptDrawerOpen(true); - }} - sx={{ - ...singleTheme.tableStyles.primary.body.row, - "cursor": "pointer", - "&:hover": { backgroundColor: palette.background.hover }, - }} - > - - - {p.id || (isMultiTurn ? `conv_${idx + 1}` : `prompt_${idx + 1}`)} - - - - - {displayText} - - - - {isMultiTurn ? ( - - ) : (p as SingleTurnPrompt).difficulty ? ( - - ) : null} - - - {isMultiTurn ? ( - - {(p as MultiTurnConversation).expected_outcome || "-"} - - ) : ( - - )} - - - { - e.stopPropagation(); - handleDeletePrompt(idx); - }} - sx={{ - "color": palette.status.error.text, - "&:hover": { backgroundColor: palette.status.error.bg }, - }} - > - - - - - ); - }) - )} - -
-
- - {/* Add prompt button */} - {editablePrompts.length > 0 && ( - } - onClick={handleAddPrompt} - fullWidth - text="Add prompt" - sx={{ - "mt": 2, - "color": palette.brand.primary, - "borderColor": palette.border.dark, - "borderStyle": "dashed", - "py": 1.5, - "&:hover": { - borderColor: palette.brand.primary, - backgroundColor: palette.brand.primaryLight, - borderStyle: "dashed", - }, - }} - /> - )} - - {/* Prompt Edit Drawer */} - { - setPromptDrawerOpen(false); - setSelectedPromptIndex(null); - }} - > - - {/* Drawer Header */} - - - Edit prompt - - { - setPromptDrawerOpen(false); - setSelectedPromptIndex(null); - }} - sx={{ cursor: "pointer" }} - > - - - - - - {selectedPromptIndex !== null && editablePrompts[selectedPromptIndex] && ( - - {/* Multi-turn conversation editor */} - {isMultiTurnConversation(editablePrompts[selectedPromptIndex]) ? ( - <> - - - Scenario - - { - const next = [...editablePrompts]; - next[selectedPromptIndex] = { - ...next[selectedPromptIndex], - scenario: e.target.value, - }; - setEditablePrompts(next); - }} - placeholder="Describe the conversation scenario" - type="description" - /> - - - - - Expected Outcome - - { - const next = [...editablePrompts]; - next[selectedPromptIndex] = { - ...next[selectedPromptIndex], - expected_outcome: e.target.value, - }; - setEditablePrompts(next); - }} - placeholder="What should the conversation achieve?" - type="description" - /> - - - - - Conversation Turns - - - {/* Chat conversation container */} - - - {( - (editablePrompts[selectedPromptIndex] as MultiTurnConversation).turns || - [] - ).map((turn, turnIdx) => ( - - - - - - {turn.role === "user" ? ( - - ) : ( - - )} - - - {turn.role === "user" ? "User" : "Assistant"} - - - { - const next = [...editablePrompts]; - const conv = next[ - selectedPromptIndex - ] as MultiTurnConversation; - const turns = [...(conv.turns || [])]; - turns.splice(turnIdx, 1); - next[selectedPromptIndex] = { ...conv, turns }; - setEditablePrompts(next); - }} - sx={{ - "p": 0.5, - "color": palette.status.error.text, - "&:hover": { backgroundColor: palette.status.error.bg }, - }} - > - - - - { - const next = [...editablePrompts]; - const conv = next[selectedPromptIndex] as MultiTurnConversation; - const turns = [...(conv.turns || [])]; - turns[turnIdx] = { ...turns[turnIdx], content: e.target.value }; - next[selectedPromptIndex] = { ...conv, turns }; - setEditablePrompts(next); - }} - placeholder={ - turn.role === "user" - ? "What does the user say?" - : "How should the assistant respond?" - } - type="description" - /> - - - ))} - - {/* Empty state when no turns */} - {( - (editablePrompts[selectedPromptIndex] as MultiTurnConversation).turns || - [] - ).length === 0 && ( - - - No conversation turns yet. Add a turn to start building the - conversation. - - - )} - - - - {/* Add turn button - at the bottom with more spacing */} - } - onClick={() => { - const next = [...editablePrompts]; - const conv = next[selectedPromptIndex] as MultiTurnConversation; - const turns = [...(conv.turns || [])]; - const lastRole = - turns.length > 0 ? turns[turns.length - 1].role : "assistant"; - turns.push({ - role: lastRole === "user" ? "assistant" : "user", - content: "", - }); - next[selectedPromptIndex] = { ...conv, turns }; - setEditablePrompts(next); - }} - sx={{ - "mt": 3, - "mb": 2, - "color": palette.brand.primary, - "borderColor": palette.border.dark, - "borderStyle": "dashed", - "py": 2, - "&:hover": { - borderColor: palette.brand.primary, - backgroundColor: palette.status.success.bg, - borderStyle: "dashed", - }, - }} - > - Add{" "} - {((editablePrompts[selectedPromptIndex] as MultiTurnConversation).turns - ?.length || 0) > 0 - ? ( - editablePrompts[selectedPromptIndex] as MultiTurnConversation - ).turns?.slice(-1)[0]?.role === "user" - ? "assistant" - : "user" - : "user"}{" "} - turn - - - - ) : ( - /* Single-turn prompt editor */ - <> - { - const next = [...editablePrompts]; - next[selectedPromptIndex] = { - ...next[selectedPromptIndex], - prompt: e.target.value, - }; - setEditablePrompts(next); - }} - placeholder="Enter the prompt text" - isRequired - type="description" - /> - - { - const next = [...editablePrompts]; - next[selectedPromptIndex] = { - ...next[selectedPromptIndex], - expected_output: e.target.value, - }; - setEditablePrompts(next); - }} - placeholder="Enter the expected response" - type="description" - /> - - - - Difficulty - - - {(["easy", "medium", "hard"] as const).map((diff) => { - const isSelected = - (editablePrompts[selectedPromptIndex] as SingleTurnPrompt) - .difficulty === diff; - return ( - { - const next = [...editablePrompts]; - next[selectedPromptIndex] = { - ...next[selectedPromptIndex], - difficulty: diff, - }; - setEditablePrompts(next); - }} - sx={{ cursor: "pointer" }} - > - - - ); - })} - - - - { - const next = [...editablePrompts]; - next[selectedPromptIndex] = { - ...next[selectedPromptIndex], - category: e.target.value, - }; - setEditablePrompts(next); - }} - placeholder="e.g., general_knowledge, coding, etc." - /> - - { - const value = e.target.value - .split(",") - .map((s) => s.trim()) - .filter(Boolean); - const next = [...editablePrompts]; - next[selectedPromptIndex] = { - ...next[selectedPromptIndex], - expected_keywords: value, - }; - setEditablePrompts(next); - }} - placeholder="Comma separated keywords" - /> - - {/* Only show retrieval context for RAG datasets */} - {editingDataset?.datasetType === "rag" && ( - { - const lines = e.target.value.split("\n"); - const next = [...editablePrompts]; - next[selectedPromptIndex] = { - ...next[selectedPromptIndex], - retrieval_context: lines, - }; - setEditablePrompts(next); - }} - placeholder="One entry per line" - type="description" - /> - )} - - )} - - - } - onClick={() => { - if (selectedPromptIndex !== null) { - handleDeletePrompt(selectedPromptIndex); - } - }} - text="Delete" - sx={{ - "color": palette.status.error.text, - "borderColor": palette.status.error.text, - "&:hover": { - borderColor: palette.status.error.text, - backgroundColor: palette.status.error.bg, - }, - "minHeight": "40px", - }} - /> - { - setPromptDrawerOpen(false); - setSelectedPromptIndex(null); - }} - text="Done" - sx={{ - minHeight: "40px", - flex: 1, - }} - /> - - - )} - - -
+ ); } // Default table view return ( - {alert && } + {ds.alert && } diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/DatasetInlineEditor.tsx b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/DatasetInlineEditor.tsx new file mode 100644 index 0000000000..3aa7799908 --- /dev/null +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/DatasetInlineEditor.tsx @@ -0,0 +1,446 @@ +/** + * @fileoverview Inline dataset editor view: name field, prompts table, and prompt edit drawer. + * + * @module pages/EvalsDashboard/ProjectDatasets/DatasetInlineEditor + */ + +import { + Box, + IconButton, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { ArrowLeft, Check, Copy, Download, Plus, Save as SaveIcon, Trash2 } from "lucide-react"; +import { CustomizableButton } from "../../../components/button/customizable-button"; +import Alert from "../../../components/Alert"; +import Chip from "../../../components/Chip"; +import Field from "../../../components/Inputs/Field"; +import singleTheme from "../../../themes/v1SingleTheme"; +import { palette } from "../../../themes/palette"; +import { + isMultiTurnConversation, + type DatasetPromptRecord, + type MultiTurnConversation, + type SingleTurnPrompt, +} from "../../../../application/repository/deepEval.repository"; +import PromptEditDrawer from "./PromptEditDrawer"; +import type { BuiltInDataset } from "./types"; + +export type DatasetInlineEditorProps = { + alert: { variant: "success" | "error"; body: string } | null; + setAlert: React.Dispatch< + React.SetStateAction<{ variant: "success" | "error"; body: string } | null> + >; + editingDataset: BuiltInDataset; + editDatasetName: string; + setEditDatasetName: React.Dispatch>; + editablePrompts: DatasetPromptRecord[]; + setEditablePrompts: React.Dispatch>; + copiedJson: boolean; + setCopiedJson: React.Dispatch>; + isValidToSave: boolean; + savingDataset: boolean; + onCloseEditor: () => void; + onSaveDataset: () => void; + onAddPrompt: () => void; + onDeletePrompt: (idx: number) => void; + promptDrawerOpen: boolean; + setPromptDrawerOpen: React.Dispatch>; + selectedPromptIndex: number | null; + setSelectedPromptIndex: React.Dispatch>; +}; + +export default function DatasetInlineEditor({ + alert, + setAlert, + editingDataset, + editDatasetName, + setEditDatasetName, + editablePrompts, + setEditablePrompts, + copiedJson, + setCopiedJson, + isValidToSave, + savingDataset, + onCloseEditor, + onSaveDataset, + onAddPrompt, + onDeletePrompt, + promptDrawerOpen, + setPromptDrawerOpen, + selectedPromptIndex, + setSelectedPromptIndex, +}: DatasetInlineEditorProps) { + const handleClosePromptDrawer = () => { + setPromptDrawerOpen(false); + setSelectedPromptIndex(null); + }; + + return ( + + {alert && } + + {/* Header with back button and save */} + + + + + + + Edit dataset + + + + { + try { + const json = JSON.stringify(editablePrompts, null, 2); + await navigator.clipboard.writeText(json); + setCopiedJson(true); + setTimeout(() => setCopiedJson(false), 2000); + } catch { + setAlert({ variant: "error", body: "Failed to copy to clipboard" }); + setTimeout(() => setAlert(null), 3000); + } + }} + startIcon={copiedJson ? : } + text={copiedJson ? "Copied!" : "Copy JSON"} + sx={{ + "color": copiedJson ? palette.status.success.text : palette.text.secondary, + "borderColor": copiedJson ? palette.status.success.text : palette.border.dark, + "&:hover": { + borderColor: palette.text.disabled, + backgroundColor: palette.background.accent, + }, + }} + /> + { + const json = JSON.stringify(editablePrompts, null, 2); + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + const slug = + editDatasetName + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_+|_+$/g, "") || "dataset"; + a.download = `${slug}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }} + startIcon={} + text="Download" + sx={{ + "color": palette.text.secondary, + "borderColor": palette.border.dark, + "&:hover": { + borderColor: palette.text.disabled, + backgroundColor: palette.background.accent, + }, + }} + /> + } + onClick={onSaveDataset} + text={savingDataset ? "Saving..." : "Save"} + /> + + + + {/* Dataset name input */} + + setEditDatasetName(e.target.value)} + placeholder="Enter a descriptive name for this dataset" + isRequired + /> + + Edit the prompts below, then click Save to update your dataset. + + + + {/* Prompts/Conversations table */} + + + + + + ID + + + {editablePrompts.length > 0 && isMultiTurnConversation(editablePrompts[0]) + ? "SCENARIO / TURNS" + : "PROMPT"} + + + {editablePrompts.length > 0 && isMultiTurnConversation(editablePrompts[0]) + ? "TURNS" + : "DIFFICULTY"} + + + {editablePrompts.length > 0 && isMultiTurnConversation(editablePrompts[0]) + ? "OUTCOME" + : "CATEGORY"} + + + + + + {editablePrompts.length === 0 ? ( + + + + No prompts in this dataset yet. + + } + onClick={onAddPrompt} + text="Add your first prompt" + sx={{ + "color": palette.brand.primary, + "borderColor": palette.brand.primary, + "&:hover": { + borderColor: palette.brand.primaryHover, + backgroundColor: palette.brand.primaryLight, + }, + }} + /> + + + ) : ( + editablePrompts.map((p, idx) => { + const isMultiTurn = isMultiTurnConversation(p); + const displayText = isMultiTurn + ? (p as MultiTurnConversation).scenario || + (p as MultiTurnConversation).turns?.[0]?.content || + "Empty conversation" + : (p as SingleTurnPrompt).prompt || "Empty prompt - click to edit"; + const hasContent = isMultiTurn + ? (p as MultiTurnConversation).turns?.length > 0 + : !!(p as SingleTurnPrompt).prompt; + + return ( + { + setSelectedPromptIndex(idx); + setPromptDrawerOpen(true); + }} + sx={{ + ...singleTheme.tableStyles.primary.body.row, + "cursor": "pointer", + "&:hover": { backgroundColor: palette.background.hover }, + }} + > + + + {p.id || (isMultiTurn ? `conv_${idx + 1}` : `prompt_${idx + 1}`)} + + + + + {displayText} + + + + {isMultiTurn ? ( + + ) : (p as SingleTurnPrompt).difficulty ? ( + + ) : null} + + + {isMultiTurn ? ( + + {(p as MultiTurnConversation).expected_outcome || "-"} + + ) : ( + + )} + + + { + e.stopPropagation(); + onDeletePrompt(idx); + }} + sx={{ + "color": palette.status.error.text, + "&:hover": { backgroundColor: palette.status.error.bg }, + }} + > + + + + + ); + }) + )} + +
+
+ + {/* Add prompt button */} + {editablePrompts.length > 0 && ( + } + onClick={onAddPrompt} + fullWidth + text="Add prompt" + sx={{ + "mt": 2, + "color": palette.brand.primary, + "borderColor": palette.border.dark, + "borderStyle": "dashed", + "py": 1.5, + "&:hover": { + borderColor: palette.brand.primary, + backgroundColor: palette.brand.primaryLight, + borderStyle: "dashed", + }, + }} + /> + )} + + +
+ ); +} diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/MyDatasetsTab.tsx b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/MyDatasetsTab.tsx new file mode 100644 index 0000000000..05afe7c923 --- /dev/null +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/MyDatasetsTab.tsx @@ -0,0 +1,182 @@ +/** + * @fileoverview My datasets tab: toolbar (filter/group/search/actions) and grouped datasets table. + * + * @module pages/EvalsDashboard/ProjectDatasets/MyDatasetsTab + */ + +import { Box, Stack } from "@mui/material"; +import { Plus, Upload } from "lucide-react"; +import { CustomizableButton } from "../../../components/button/customizable-button"; +import SearchBox from "../../../components/Search/SearchBox"; +import { + FilterBy, + type FilterColumn, + type FilterCondition, +} from "../../../components/Table/FilterBy"; +import { GroupBy } from "../../../components/Table/GroupBy"; +import { GroupedTableView } from "../../../components/Table/GroupedTableView"; +import DatasetsTable, { type DatasetRow } from "../../../components/Table/DatasetsTable"; +import { palette } from "../../../themes/palette"; +import type { GroupedData } from "../../../../application/hooks/useTableGrouping"; +import type { BuiltInDataset } from "./types"; + +export type MyDatasetsTabProps = { + filterColumns: FilterColumn[]; + onFilterChange: (conditions: FilterCondition[], logic: "and" | "or") => void; + onGroupChange: (field: string | null, sortOrder: "asc" | "desc") => void; + searchTerm: string; + onSearchTermChange: (value: string) => void; + uploading: boolean; + canUploadDataset: boolean; + canDeleteDataset: boolean; + onUploadClick: () => void; + onCreateClick: () => void; + groupedDatasets: GroupedData[] | null; + filteredDatasets: BuiltInDataset[]; + loading: boolean; + onRowClick: (dataset: BuiltInDataset) => void; + onView: (dataset: BuiltInDataset) => void; + onEdit: (dataset: BuiltInDataset) => void; + onDelete: (dataset: BuiltInDataset) => void; + onDownload: (dataset: BuiltInDataset) => void; +}; + +export default function MyDatasetsTab({ + filterColumns, + onFilterChange, + onGroupChange, + searchTerm, + onSearchTermChange, + uploading, + canUploadDataset, + canDeleteDataset, + onUploadClick, + onCreateClick, + groupedDatasets, + filteredDatasets, + loading, + onRowClick, + onView, + onEdit, + onDelete, + onDownload, +}: MyDatasetsTabProps) { + return ( + <> + {/* Filters + search + upload + create */} + + + + + + + + } + onClick={onUploadClick} + isDisabled={uploading || !canUploadDataset} + sx={{ + border: `1px solid ${palette.border.dark}`, + color: palette.text.secondary, + gap: 2, + }} + /> + } + onClick={onCreateClick} + isDisabled={!canUploadDataset} + sx={{ + backgroundColor: palette.brand.primary, + border: `1px solid ${palette.brand.primary}`, + gap: 2, + }} + /> + + + + {/* Table of user datasets */} + + ( + ({ + key: dataset.path, + name: dataset.name, + path: dataset.path, + type: dataset.turnType, + useCase: dataset.use_case || dataset.datasetType, + createdAt: dataset.createdAt, + metadata: { + promptCount: dataset.promptCount ?? 0, + avgDifficulty: "Medium", // Default - only shown for user datasets + loading: false, + }, + }), + )} + onRowClick={ + canUploadDataset + ? (row) => { + const dataset = data.find((d) => d.path === row.path); + if (dataset) onRowClick(dataset); + } + : undefined + } + onView={(row) => { + const dataset = data.find((d) => d.path === row.path); + if (dataset) onView(dataset); + }} + onEdit={ + canUploadDataset + ? (row) => { + const dataset = data.find((d) => d.path === row.path); + if (dataset) onEdit(dataset); + } + : undefined + } + onDelete={ + canDeleteDataset + ? (row) => { + const dataset = data.find((d) => d.path === row.path); + if (dataset) onDelete(dataset); + } + : undefined + } + onDownload={(row: DatasetRow) => { + const dataset = data.find((d) => d.path === row.path); + if (dataset) onDownload(dataset); + }} + loading={loading} + hidePagination={options?.hidePagination} + /> + )} + /> + + + ); +} diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/PromptEditDrawer.tsx b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/PromptEditDrawer.tsx new file mode 100644 index 0000000000..eeff80f342 --- /dev/null +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/PromptEditDrawer.tsx @@ -0,0 +1,468 @@ +/** + * @fileoverview Side drawer for editing a single prompt or multi-turn conversation in the dataset inline editor. + * + * @module pages/EvalsDashboard/ProjectDatasets/PromptEditDrawer + */ + +import { Box, Divider, Drawer, IconButton, Stack, Typography, useTheme } from "@mui/material"; +import { Bot, Plus, Trash2, User, X } from "lucide-react"; +import { CustomizableButton } from "../../../components/button/customizable-button"; +import Chip from "../../../components/Chip"; +import Field from "../../../components/Inputs/Field"; +import { palette } from "../../../themes/palette"; +import { + isMultiTurnConversation, + type DatasetPromptRecord, + type MultiTurnConversation, + type SingleTurnPrompt, +} from "../../../../application/repository/deepEval.repository"; +import type { BuiltInDataset } from "./types"; + +export type PromptEditDrawerProps = { + open: boolean; + onClose: () => void; + selectedPromptIndex: number | null; + editablePrompts: DatasetPromptRecord[]; + setEditablePrompts: React.Dispatch>; + editingDataset: BuiltInDataset | null; + onDeletePrompt: (idx: number) => void; +}; + +export default function PromptEditDrawer({ + open, + onClose, + selectedPromptIndex, + editablePrompts, + setEditablePrompts, + editingDataset, + onDeletePrompt, +}: PromptEditDrawerProps) { + const theme = useTheme(); + + return ( + + + {/* Drawer Header */} + + + Edit prompt + + + + + + + + {selectedPromptIndex !== null && editablePrompts[selectedPromptIndex] && ( + + {/* Multi-turn conversation editor */} + {isMultiTurnConversation(editablePrompts[selectedPromptIndex]) ? ( + <> + + + Scenario + + { + const next = [...editablePrompts]; + next[selectedPromptIndex] = { + ...next[selectedPromptIndex], + scenario: e.target.value, + }; + setEditablePrompts(next); + }} + placeholder="Describe the conversation scenario" + type="description" + /> + + + + + Expected Outcome + + { + const next = [...editablePrompts]; + next[selectedPromptIndex] = { + ...next[selectedPromptIndex], + expected_outcome: e.target.value, + }; + setEditablePrompts(next); + }} + placeholder="What should the conversation achieve?" + type="description" + /> + + + + + Conversation Turns + + + {/* Chat conversation container */} + + + {( + (editablePrompts[selectedPromptIndex] as MultiTurnConversation).turns || [] + ).map((turn, turnIdx) => ( + + + + + + {turn.role === "user" ? ( + + ) : ( + + )} + + + {turn.role === "user" ? "User" : "Assistant"} + + + { + const next = [...editablePrompts]; + const conv = next[selectedPromptIndex] as MultiTurnConversation; + const turns = [...(conv.turns || [])]; + turns.splice(turnIdx, 1); + next[selectedPromptIndex] = { ...conv, turns }; + setEditablePrompts(next); + }} + sx={{ + "p": 0.5, + "color": palette.status.error.text, + "&:hover": { backgroundColor: palette.status.error.bg }, + }} + > + + + + { + const next = [...editablePrompts]; + const conv = next[selectedPromptIndex] as MultiTurnConversation; + const turns = [...(conv.turns || [])]; + turns[turnIdx] = { ...turns[turnIdx], content: e.target.value }; + next[selectedPromptIndex] = { ...conv, turns }; + setEditablePrompts(next); + }} + placeholder={ + turn.role === "user" + ? "What does the user say?" + : "How should the assistant respond?" + } + type="description" + /> + + + ))} + + {/* Empty state when no turns */} + {((editablePrompts[selectedPromptIndex] as MultiTurnConversation).turns || []) + .length === 0 && ( + + + No conversation turns yet. Add a turn to start building the + conversation. + + + )} + + + + {/* Add turn button - at the bottom with more spacing */} + } + onClick={() => { + const next = [...editablePrompts]; + const conv = next[selectedPromptIndex] as MultiTurnConversation; + const turns = [...(conv.turns || [])]; + const lastRole = + turns.length > 0 ? turns[turns.length - 1].role : "assistant"; + turns.push({ + role: lastRole === "user" ? "assistant" : "user", + content: "", + }); + next[selectedPromptIndex] = { ...conv, turns }; + setEditablePrompts(next); + }} + sx={{ + "mt": 3, + "mb": 2, + "color": palette.brand.primary, + "borderColor": palette.border.dark, + "borderStyle": "dashed", + "py": 2, + "&:hover": { + borderColor: palette.brand.primary, + backgroundColor: palette.status.success.bg, + borderStyle: "dashed", + }, + }} + > + Add{" "} + {((editablePrompts[selectedPromptIndex] as MultiTurnConversation).turns + ?.length || 0) > 0 + ? ( + editablePrompts[selectedPromptIndex] as MultiTurnConversation + ).turns?.slice(-1)[0]?.role === "user" + ? "assistant" + : "user" + : "user"}{" "} + turn + + + + ) : ( + /* Single-turn prompt editor */ + <> + { + const next = [...editablePrompts]; + next[selectedPromptIndex] = { + ...next[selectedPromptIndex], + prompt: e.target.value, + }; + setEditablePrompts(next); + }} + placeholder="Enter the prompt text" + isRequired + type="description" + /> + + { + const next = [...editablePrompts]; + next[selectedPromptIndex] = { + ...next[selectedPromptIndex], + expected_output: e.target.value, + }; + setEditablePrompts(next); + }} + placeholder="Enter the expected response" + type="description" + /> + + + + Difficulty + + + {(["easy", "medium", "hard"] as const).map((diff) => { + const isSelected = + (editablePrompts[selectedPromptIndex] as SingleTurnPrompt).difficulty === + diff; + return ( + { + const next = [...editablePrompts]; + next[selectedPromptIndex] = { + ...next[selectedPromptIndex], + difficulty: diff, + }; + setEditablePrompts(next); + }} + sx={{ cursor: "pointer" }} + > + + + ); + })} + + + + { + const next = [...editablePrompts]; + next[selectedPromptIndex] = { + ...next[selectedPromptIndex], + category: e.target.value, + }; + setEditablePrompts(next); + }} + placeholder="e.g., general_knowledge, coding, etc." + /> + + { + const value = e.target.value + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + const next = [...editablePrompts]; + next[selectedPromptIndex] = { + ...next[selectedPromptIndex], + expected_keywords: value, + }; + setEditablePrompts(next); + }} + placeholder="Comma separated keywords" + /> + + {/* Only show retrieval context for RAG datasets */} + {editingDataset?.datasetType === "rag" && ( + { + const lines = e.target.value.split("\n"); + const next = [...editablePrompts]; + next[selectedPromptIndex] = { + ...next[selectedPromptIndex], + retrieval_context: lines, + }; + setEditablePrompts(next); + }} + placeholder="One entry per line" + type="description" + /> + )} + + )} + + + } + onClick={() => { + if (selectedPromptIndex !== null) { + onDeletePrompt(selectedPromptIndex); + } + }} + text="Delete" + sx={{ + "color": palette.status.error.text, + "borderColor": palette.status.error.text, + "&:hover": { + borderColor: palette.status.error.text, + backgroundColor: palette.status.error.bg, + }, + "minHeight": "40px", + }} + /> + + + + )} + + + ); +} diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/TemplatesTab.tsx b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/TemplatesTab.tsx new file mode 100644 index 0000000000..d199173b02 --- /dev/null +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/TemplatesTab.tsx @@ -0,0 +1,99 @@ +/** + * @fileoverview Templates tab: toolbar (filter/group/search) and built-in templates table. + * + * @module pages/EvalsDashboard/ProjectDatasets/TemplatesTab + */ + +import { Box, Stack } from "@mui/material"; +import SearchBox from "../../../components/Search/SearchBox"; +import { + FilterBy, + type FilterColumn, + type FilterCondition, +} from "../../../components/Table/FilterBy"; +import { GroupBy } from "../../../components/Table/GroupBy"; +import TemplatesTable from "../../../components/Table/TemplatesTable"; +import type { BuiltInDataset, TemplateWithCategory } from "./types"; + +export type TemplatesTabProps = { + filterColumns: FilterColumn[]; + onFilterChange: (conditions: FilterCondition[], logic: "and" | "or") => void; + templateSearchTerm: string; + onTemplateSearchTermChange: (value: string) => void; + filteredTemplates: TemplateWithCategory[]; + flattenedTemplatesCount: number; + loading: boolean; + copyingTemplate: boolean; + templateGroups: Record<"chatbot" | "rag" | "agent", BuiltInDataset[]>; + onViewTemplate: (template: BuiltInDataset) => void; + onCopyTemplate: (template: BuiltInDataset) => void; +}; + +export default function TemplatesTab({ + filterColumns, + onFilterChange, + templateSearchTerm, + onTemplateSearchTermChange, + filteredTemplates, + flattenedTemplatesCount, + loading, + copyingTemplate, + templateGroups, + onViewTemplate, + onCopyTemplate, +}: TemplatesTabProps) { + return ( + + {/* Filter + search toolbar */} + + + {}} + /> + + + + ({ + key: ds.key, + name: ds.name, + path: ds.path, + type: ds.type as "single-turn" | "multi-turn" | "simulated" | undefined, + category: ds.category, + test_count: ds.test_count, + difficulty: ds.difficulty, + description: ds.description, + }))} + loading={loading} + onRowClick={(template) => + onViewTemplate( + templateGroups[template.category]?.find((t) => t.key === template.key) || + (template as unknown as BuiltInDataset), + ) + } + onUse={(template) => + onCopyTemplate( + templateGroups[template.category]?.find((t) => t.key === template.key) || + (template as unknown as BuiltInDataset), + ) + } + copyingTemplate={copyingTemplate} + emptyMessage={ + flattenedTemplatesCount === 0 + ? "No template datasets available" + : "No templates match your search" + } + /> + + ); +} diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/types.ts b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/types.ts new file mode 100644 index 0000000000..a243bae181 --- /dev/null +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/types.ts @@ -0,0 +1,29 @@ +/** + * @fileoverview Shared types for the Project Datasets page and its extracted subcomponents. + * + * @module pages/EvalsDashboard/ProjectDatasets/types + */ + +import type { + DatasetType, + ListedDataset, +} from "../../../../application/repository/deepEval.repository"; + +export type BuiltInDataset = ListedDataset & { + promptCount?: number; + isUserDataset?: boolean; + createdAt?: string; + datasetType?: DatasetType; + turnType?: "single-turn" | "multi-turn" | "simulated"; + // Additional metadata for templates + test_count?: number; + categories?: string[]; + category_count?: number; + difficulty?: { easy: number; medium: number; hard: number }; + description?: string; + tags?: string[]; +}; + +export type TemplateWithCategory = BuiltInDataset & { + category: "chatbot" | "rag" | "agent"; +}; diff --git a/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/useProjectDatasets.ts b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/useProjectDatasets.ts new file mode 100644 index 0000000000..972b3f781d --- /dev/null +++ b/Clients/src/presentation/pages/EvalsDashboard/ProjectDatasets/useProjectDatasets.ts @@ -0,0 +1,704 @@ +/** + * @fileoverview State, loaders, filters, and CRUD handlers for the Project Datasets page. + * + * @module pages/EvalsDashboard/ProjectDatasets/useProjectDatasets + */ + +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + listMyDatasets, + listDatasets, + readDataset, + uploadDataset, + deleteDatasets, + type DatasetPromptRecord, +} from "../../../../application/repository/deepEval.repository"; +import { + isSingleTurnPrompt, + isMultiTurnConversation, +} from "../../../../application/repository/deepEval.repository"; +import type { FilterColumn } from "../../../components/Table/FilterBy"; +import { useTableGrouping, useGroupByState } from "../../../../application/hooks/useTableGrouping"; +import { useFilterBy } from "../../../../application/hooks/useFilterBy"; +import { useAuth } from "../../../../application/hooks/useAuth"; +import allowedRoles from "../../../../application/constants/permissions"; +import type { ExampleTurnType, ExampleUseCase } from "./exampleDatasetPayloads"; +import type { BuiltInDataset, TemplateWithCategory } from "./types"; + +export type UseProjectDatasetsArgs = { + orgId?: string | null; +}; + +export function useProjectDatasets({ orgId }: UseProjectDatasetsArgs) { + // RBAC permissions + const { userRoleName, isSuperAdmin } = useAuth(); + const canUploadDataset = allowedRoles.evals.uploadDataset.includes(userRoleName) && !isSuperAdmin; + const canDeleteDataset = allowedRoles.evals.deleteDataset.includes(userRoleName) && !isSuperAdmin; + + // Tab state: "my" for user datasets, "templates" for built-in datasets + const [activeTab, setActiveTab] = useState<"my" | "templates">("my"); + + // My datasets state + const [datasets, setDatasets] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingTemplatesList, setLoadingTemplatesList] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const { + groupBy: datasetsGroupBy, + groupSortOrder: datasetsGroupSortOrder, + handleGroupChange: handleDatasetsGroupChange, + } = useGroupByState(); + const [alert, setAlert] = useState<{ variant: "success" | "error"; body: string } | null>(null); + const [uploading, setUploading] = useState(false); + const [uploadModalOpen, setUploadModalOpen] = useState(false); + const fileInputRef = useRef(null); + const [drawerOpen, setDrawerOpen] = useState(false); + const [selectedDataset, setSelectedDataset] = useState(null); + const [datasetPrompts, setDatasetPrompts] = useState([]); + const [loadingPrompts, setLoadingPrompts] = useState(false); + + // Template datasets state + const [templateGroups, setTemplateGroups] = useState< + Record<"chatbot" | "rag" | "agent", BuiltInDataset[]> + >({ + chatbot: [], + rag: [], + agent: [], + }); + const [selectedTemplate, setSelectedTemplate] = useState(null); + const [templatePrompts, setTemplatePrompts] = useState([]); + const [loadingTemplatePrompts, setLoadingTemplatePrompts] = useState(false); + const [copyingTemplate, setCopyingTemplate] = useState(false); + + // Template table state (search) - sorting and pagination handled by TemplatesTable component + const [templateSearchTerm, setTemplateSearchTerm] = useState(""); + + // Copy template confirmation modal state + const [copyModalOpen, setCopyModalOpen] = useState(false); + const [templateToCopy, setTemplateToCopy] = useState(null); + + // Template drawer state + const [templateDrawerOpen, setTemplateDrawerOpen] = useState(false); + + // Delete modal state + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [datasetToDelete, setDatasetToDelete] = useState(null); + + // Inline editor state + const [editorOpen, setEditorOpen] = useState(false); + const [editingDataset, setEditingDataset] = useState(null); + const [editablePrompts, setEditablePrompts] = useState([]); + const [editDatasetName, setEditDatasetName] = useState(""); + const [savingDataset, setSavingDataset] = useState(false); + const [loadingEditor, setLoadingEditor] = useState(false); + const [copiedJson, setCopiedJson] = useState(false); + + // Prompt edit drawer state (for inline editor) + const [promptDrawerOpen, setPromptDrawerOpen] = useState(false); + const [selectedPromptIndex, setSelectedPromptIndex] = useState(null); + + // Create dataset modal state + const [createDatasetModalOpen, setCreateDatasetModalOpen] = useState(false); + const [createTypeSelectionOpen, setCreateTypeSelectionOpen] = useState(false); + + // Example dataset type for download / upload metadata + const [exampleDatasetType, setExampleDatasetType] = useState("chatbot"); + const [datasetTurnType, setDatasetTurnType] = useState("single-turn"); + + // Note: promptCount is now returned by the API - no need to load metadata individually + + // Load user's datasets (My datasets tab) + const loadMyDatasets = useCallback(async () => { + try { + setLoading(true); + const userRes = await listMyDatasets().catch(() => ({ datasets: [] })); + const userDatasets = userRes.datasets || []; + const allDatasets: BuiltInDataset[] = userDatasets.map((ud) => ({ + key: `user_${ud.id}`, + name: ud.name, + path: ud.path, + use_case: (ud.datasetType || "chatbot") as "chatbot" | "rag" | "agent", + datasetType: ud.datasetType || "chatbot", + turnType: ud.turnType, + isUserDataset: true, + createdAt: ud.createdAt, + promptCount: ud.promptCount || 0, // Use pre-computed count from API + })); + setDatasets(allDatasets); + } catch (err) { + console.error("Failed to load datasets", err); + setDatasets([]); + setAlert({ + variant: "error", + body: "Failed to load datasets", + }); + setTimeout(() => setAlert(null), 5000); + } finally { + setLoading(false); + } + }, []); + + // Load built-in template datasets (Templates tab) + const loadTemplateDatasets = useCallback(async () => { + try { + setLoadingTemplatesList(true); + const res = await listDatasets(); + setTemplateGroups(res as Record<"chatbot" | "rag" | "agent", BuiltInDataset[]>); + } catch (err) { + console.error("Failed to load template datasets", err); + setTemplateGroups({ chatbot: [], rag: [], agent: [] }); + setAlert({ + variant: "error", + body: "Failed to load template datasets", + }); + setTimeout(() => setAlert(null), 5000); + } finally { + setLoadingTemplatesList(false); + } + }, []); + + // Flatten templates from all categories into a single array with category field + const flattenedTemplates: TemplateWithCategory[] = useMemo(() => { + return (["chatbot", "rag", "agent"] as const).flatMap((category) => + (templateGroups[category] || []).map((ds) => ({ ...ds, category })), + ); + }, [templateGroups]); + + // Template filter columns + const templateFilterColumns: FilterColumn[] = useMemo( + () => [ + { id: "name", label: "Dataset name", type: "text" }, + { + id: "category", + label: "Category", + type: "select", + options: [ + { value: "chatbot", label: "Chatbot" }, + { value: "rag", label: "RAG" }, + { value: "agent", label: "Agent" }, + ], + }, + ], + [], + ); + + // Template field value getter for filtering + const getTemplateFieldValue = useCallback( + (item: TemplateWithCategory, fieldId: string): string | number | Date | null | undefined => { + switch (fieldId) { + case "name": + return item.name; + case "category": + return item.category; + default: + return null; + } + }, + [], + ); + + // useFilterBy hook for templates + const { filterData: filterTemplateData, handleFilterChange: handleTemplateFilterChange } = + useFilterBy(getTemplateFieldValue); + + // Filtered templates (sorting and pagination handled by TemplatesTable component) + const filteredAndSortedTemplates = useMemo(() => { + // Apply filter + let result = filterTemplateData(flattenedTemplates); + + // Apply search + if (templateSearchTerm.trim()) { + const q = templateSearchTerm.toLowerCase(); + result = result.filter( + (t) => t.name.toLowerCase().includes(q) || t.category.toLowerCase().includes(q), + ); + } + + return result; + }, [flattenedTemplates, filterTemplateData, templateSearchTerm]); + + // Open copy confirmation modal + const handleOpenCopyModal = (template: BuiltInDataset) => { + setTemplateToCopy(template); + setCopyModalOpen(true); + }; + + // Confirm copy + const handleConfirmCopy = async () => { + if (!templateToCopy) return; + setCopyModalOpen(false); + await handleCopyTemplate(templateToCopy); + setTemplateToCopy(null); + }; + + // Open template drawer + const handleViewTemplate = (template: BuiltInDataset) => { + setSelectedTemplate(template); + setTemplateDrawerOpen(true); + }; + + // Close template drawer + const handleCloseTemplateDrawer = () => { + setTemplateDrawerOpen(false); + setSelectedTemplate(null); + setTemplatePrompts([]); + }; + + // Load both datasets on mount so tab counts are always available + useEffect(() => { + void loadMyDatasets(); + void loadTemplateDatasets(); + }, [loadMyDatasets, loadTemplateDatasets]); + + // Load template prompts when a template is selected + useEffect(() => { + if (!selectedTemplate?.path) { + setTemplatePrompts([]); + return; + } + (async () => { + try { + setLoadingTemplatePrompts(true); + const res = await readDataset(selectedTemplate.path); + setTemplatePrompts(res.prompts || []); + } catch (err) { + console.error("Failed to load template prompts", err); + setTemplatePrompts([]); + } finally { + setLoadingTemplatePrompts(false); + } + })(); + }, [selectedTemplate]); + + // Copy template to user's datasets + const handleCopyTemplate = async (template: BuiltInDataset) => { + try { + setCopyingTemplate(true); + // Load the template content + const res = await readDataset(template.path); + const prompts = res.prompts || []; + + // Create a new file and upload it + const json = JSON.stringify(prompts, null, 2); + const blob = new Blob([json], { type: "application/json" }); + const fileName = `${template.name.toLowerCase().replace(/[^a-z0-9]+/g, "_")}.json`; + const file = new File([blob], fileName, { type: "application/json" }); + + await uploadDataset(file, "chatbot", "single-turn", orgId || undefined); + setAlert({ variant: "success", body: `"${template.name}" copied to your datasets` }); + setTimeout(() => setAlert(null), 3000); + + // Switch to My datasets tab and reload + setActiveTab("my"); + void loadMyDatasets(); + } catch (err) { + console.error("Failed to copy template", err); + setAlert({ variant: "error", body: "Failed to copy template" }); + setTimeout(() => setAlert(null), 5000); + } finally { + setCopyingTemplate(false); + } + }; + + const filterColumns: FilterColumn[] = useMemo( + () => [{ id: "name", label: "Dataset name", type: "text" }], + [], + ); + + const getFieldValue = useCallback( + (d: BuiltInDataset, fieldId: string): string | number | Date | null | undefined => { + switch (fieldId) { + case "name": + return d.name; + case "use_case": + return d.use_case; + default: + return ""; + } + }, + [], + ); + + const { filterData, handleFilterChange } = useFilterBy(getFieldValue); + + const filteredDatasets = useMemo(() => { + const afterFilter = filterData(datasets); + if (!searchTerm.trim()) return afterFilter; + const q = searchTerm.toLowerCase(); + return afterFilter.filter((d) => + [d.name, d.path, d.use_case].filter(Boolean).join(" ").toLowerCase().includes(q), + ); + }, [datasets, filterData, searchTerm]); + + // Datasets grouping + const getDatasetGroupKey = useCallback((dataset: BuiltInDataset, field: string): string => { + switch (field) { + case "name": + // Group by first letter + return dataset.name?.charAt(0).toUpperCase() || "Other"; + case "prompts": { + const count = dataset.promptCount ?? 0; + if (count === 0) return "No prompts"; + if (count <= 10) return "1-10 prompts"; + if (count <= 50) return "11-50 prompts"; + if (count <= 100) return "51-100 prompts"; + return "100+ prompts"; + } + case "createdAt": { + if (!dataset.createdAt) return "Unknown"; + const date = new Date(dataset.createdAt); + const now = new Date(); + const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24)); + if (diffDays === 0) return "Today"; + if (diffDays === 1) return "Yesterday"; + if (diffDays <= 7) return "This week"; + if (diffDays <= 30) return "This month"; + return "Older"; + } + default: + return "Other"; + } + }, []); + + const groupedDatasets = useTableGrouping({ + data: filteredDatasets, + groupByField: datasetsGroupBy, + sortOrder: datasetsGroupSortOrder, + getGroupKey: getDatasetGroupKey, + }); + + const handleViewPrompts = async (dataset: BuiltInDataset) => { + setSelectedDataset(dataset); + setDrawerOpen(true); + try { + setLoadingPrompts(true); + const res = await readDataset(dataset.path); + setDatasetPrompts(res.prompts || []); + } catch (err) { + console.error("Failed to load dataset prompts", err); + setDatasetPrompts([]); + } finally { + setLoadingPrompts(false); + } + }; + + const handleOpenInEditor = async (dataset: BuiltInDataset) => { + try { + setLoadingEditor(true); + const res = await readDataset(dataset.path); + setEditablePrompts(res.prompts || []); + // Use the dataset name directly (already cleaned by backend) + setEditDatasetName(dataset.name); + setEditingDataset(dataset); + setEditorOpen(true); + } catch (err) { + console.error("Failed to load dataset for editing", err); + setAlert({ variant: "error", body: "Failed to load dataset for editing" }); + setTimeout(() => setAlert(null), 5000); + } finally { + setLoadingEditor(false); + } + }; + + const handleCloseEditor = () => { + setEditorOpen(false); + setEditingDataset(null); + setEditablePrompts([]); + setEditDatasetName(""); + }; + + const handleSaveDataset = async () => { + try { + setSavingDataset(true); + const json = JSON.stringify(editablePrompts, null, 2); + const blob = new Blob([json], { type: "application/json" }); + const slug = editDatasetName + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_+|_+$/g, ""); + const finalName = slug ? `${slug}.json` : "dataset.json"; + const file = new File([blob], finalName, { type: "application/json" }); + const datasetType = editingDataset?.datasetType || "chatbot"; + + // If editing an existing user dataset, delete the old one first + if (editingDataset?.isUserDataset && editingDataset?.path) { + try { + await deleteDatasets([editingDataset.path]); + } catch (deleteErr) { + console.warn("Could not delete old dataset, proceeding with save:", deleteErr); + } + } + + const turnType = + editablePrompts.length > 0 && !isSingleTurnPrompt(editablePrompts[0]) + ? "multi-turn" + : "single-turn"; + await uploadDataset(file, datasetType, turnType, orgId || undefined); + setAlert({ variant: "success", body: `Dataset "${editDatasetName}" saved successfully!` }); + setTimeout(() => setAlert(null), 3000); + handleCloseEditor(); + void loadMyDatasets(); + } catch (err) { + console.error("Failed to save dataset", err); + type AxiosLike = { response?: { data?: unknown } }; + const axiosErr = err as AxiosLike | Error; + const resData = (axiosErr as AxiosLike)?.response?.data as + | Record + | undefined; + const serverMsg = + (resData && (String(resData.message ?? "") || String(resData.detail ?? ""))) || + (axiosErr instanceof Error ? axiosErr.message : null); + setAlert({ variant: "error", body: serverMsg || "Save failed. Check dataset structure." }); + setTimeout(() => setAlert(null), 6000); + } finally { + setSavingDataset(false); + } + }; + + const isValidToSave = useMemo(() => { + if (!editablePrompts || editablePrompts.length === 0 || !editDatasetName.trim()) return false; + // Check if any record has valid content (single-turn prompt or multi-turn turns) + return editablePrompts.some((p) => { + if (isSingleTurnPrompt(p)) { + return p.prompt.trim().length > 0; + } else if (isMultiTurnConversation(p)) { + return p.turns && p.turns.length > 0 && p.turns.some((t) => t.content.trim().length > 0); + } + return false; + }); + }, [editablePrompts, editDatasetName]); + + const handleAddPrompt = () => { + const newPrompt: DatasetPromptRecord = { + id: `prompt_${Date.now()}`, + category: "General", + prompt: "", + expected_output: "", + expected_keywords: [], + retrieval_context: [], + }; + setEditablePrompts((prev) => [...prev, newPrompt]); + // Open the drawer with the new prompt + setSelectedPromptIndex(editablePrompts.length); + setPromptDrawerOpen(true); + }; + + const handleDeletePrompt = (idx: number) => { + setEditablePrompts((prev) => prev.filter((_, i) => i !== idx)); + if (selectedPromptIndex === idx) { + setPromptDrawerOpen(false); + setSelectedPromptIndex(null); + } else if (selectedPromptIndex !== null && selectedPromptIndex > idx) { + setSelectedPromptIndex(selectedPromptIndex - 1); + } + }; + + const handleConfirmDelete = async () => { + if (!datasetToDelete) return; + try { + await deleteDatasets([datasetToDelete.path]); + setAlert({ variant: "success", body: "Dataset removed" }); + setTimeout(() => setAlert(null), 3000); + void loadMyDatasets(); + } catch (err) { + console.error("Failed to remove dataset", err); + setAlert({ variant: "error", body: "Failed to remove dataset" }); + setTimeout(() => setAlert(null), 5000); + } finally { + setDeleteModalOpen(false); + setDatasetToDelete(null); + } + }; + + const handleRowClick = async (dataset: BuiltInDataset) => { + // Navigate directly to editor when clicking on a row + await handleOpenInEditor(dataset); + }; + + const handleDownloadDataset = async (dataset: BuiltInDataset) => { + try { + const res = await readDataset(dataset.path); + const json = JSON.stringify(res.prompts || [], null, 2); + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + const slug = + dataset.name + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_+|_+$/g, "") || "dataset"; + a.download = `${slug}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (err) { + console.error("Failed to download dataset", err); + setAlert({ variant: "error", body: "Failed to download dataset" }); + setTimeout(() => setAlert(null), 5000); + } + }; + + // Metadata is now pre-computed by backend - no need to load individually + + const handleCloseDrawer = () => { + setDrawerOpen(false); + setSelectedDataset(null); + setDatasetPrompts([]); + }; + + const handleUploadClick = () => { + setUploadModalOpen(true); + }; + + const handleFileSelect = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + try { + setUploading(true); + setUploadModalOpen(false); + const resp = await uploadDataset( + file, + exampleDatasetType, + datasetTurnType, + orgId || undefined, + ); + setAlert({ variant: "success", body: `Uploaded ${resp.filename}` }); + setTimeout(() => setAlert(null), 4000); + void loadMyDatasets(); + } catch (err) { + console.error("Upload failed", err); + setAlert({ + variant: "error", + body: err instanceof Error ? err.message : "Upload failed", + }); + setTimeout(() => setAlert(null), 6000); + } finally { + setUploading(false); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + } + }; + + const handleRequestDelete = (dataset: BuiltInDataset) => { + setDatasetToDelete(dataset); + setDeleteModalOpen(true); + }; + + return { + // RBAC + canUploadDataset, + canDeleteDataset, + + // Tabs + activeTab, + setActiveTab, + + // My datasets + datasets, + loading, + searchTerm, + setSearchTerm, + filterColumns, + handleFilterChange, + handleDatasetsGroupChange, + filteredDatasets, + groupedDatasets, + + // Templates + loadingTemplatesList, + templateGroups, + flattenedTemplates, + templateFilterColumns, + handleTemplateFilterChange, + templateSearchTerm, + setTemplateSearchTerm, + filteredAndSortedTemplates, + selectedTemplate, + setSelectedTemplate, + templatePrompts, + loadingTemplatePrompts, + copyingTemplate, + templateDrawerOpen, + handleViewTemplate, + handleCloseTemplateDrawer, + handleOpenCopyModal, + copyModalOpen, + setCopyModalOpen, + templateToCopy, + setTemplateToCopy, + handleConfirmCopy, + + // Alert + alert, + setAlert, + + // Upload + uploading, + uploadModalOpen, + setUploadModalOpen, + fileInputRef, + exampleDatasetType, + setExampleDatasetType, + datasetTurnType, + setDatasetTurnType, + handleUploadClick, + handleFileSelect, + handleFileChange, + + // Preview drawer + drawerOpen, + selectedDataset, + datasetPrompts, + loadingPrompts, + handleViewPrompts, + handleCloseDrawer, + + // Delete + deleteModalOpen, + setDeleteModalOpen, + datasetToDelete, + setDatasetToDelete, + handleConfirmDelete, + handleRequestDelete, + + // Editor + editorOpen, + setEditorOpen, + editingDataset, + setEditingDataset, + editablePrompts, + setEditablePrompts, + editDatasetName, + setEditDatasetName, + savingDataset, + loadingEditor, + copiedJson, + setCopiedJson, + isValidToSave, + promptDrawerOpen, + setPromptDrawerOpen, + selectedPromptIndex, + setSelectedPromptIndex, + handleOpenInEditor, + handleCloseEditor, + handleSaveDataset, + handleAddPrompt, + handleDeletePrompt, + handleRowClick, + handleDownloadDataset, + + // Create modals + createDatasetModalOpen, + setCreateDatasetModalOpen, + createTypeSelectionOpen, + setCreateTypeSelectionOpen, + }; +} From cc9d4b660111efd7dbea25857af3d55d288099b2 Mon Sep 17 00:00:00 2001 From: Inna Glamazda Date: Mon, 10 Aug 2026 17:27:20 -0400 Subject: [PATCH 6/8] fix(clients): bump nanoid overrides to clear high audit advisories Force nanoid@3 to 3.3.17 and nanoid@5 to 5.1.16 so the frontend audit gate passes GHSA-28wg-ghj8-5hjv and GHSA-2v37-7h3g-55p8. Co-authored-by: Cursor --- Clients/package-lock.json | 12 ++++++------ Clients/package.json | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Clients/package-lock.json b/Clients/package-lock.json index 3b505feae1..eb8dd77338 100644 --- a/Clients/package-lock.json +++ b/Clients/package-lock.json @@ -12860,9 +12860,9 @@ } }, "node_modules/nanoid": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", - "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==", + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", "funding": [ { "type": "github", @@ -13228,9 +13228,9 @@ "license": "MIT" }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "funding": [ { "type": "github", diff --git a/Clients/package.json b/Clients/package.json index ac7bdfe16a..0208d60f2f 100644 --- a/Clients/package.json +++ b/Clients/package.json @@ -154,6 +154,8 @@ "brace-expansion@2": "^2.1.4", "brace-expansion@3": "^3.0.6", "brace-expansion@4": "^5.0.9", - "brace-expansion@5": "^5.0.9" + "brace-expansion@5": "^5.0.9", + "nanoid@3": "3.3.17", + "nanoid@5": "5.1.16" } } From 2a10d014780baecfe683032c70f3266cc1d2c543 Mon Sep 17 00:00:00 2001 From: Inna Glamazda Date: Tue, 11 Aug 2026 19:58:55 -0400 Subject: [PATCH 7/8] ci(e2e): work around AIGateway cryptography/presidio install conflict Install presidio-anonymizer with --no-deps so E2E can keep cryptography>=50 while Presidio still caps it below 49. Co-authored-by: Cursor --- .github/workflows/e2e-tests.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index c8bf13e7e4..6973da68c8 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -91,7 +91,12 @@ jobs: - name: Install AIGateway dependencies working-directory: AIGateway - run: pip install -r requirements.txt + run: | + # presidio-anonymizer 2.2.364 pins cryptography<49, which conflicts + # with cryptography>=50 (CVE-2026-69247). Install it without that pin. + grep -v '^presidio-anonymizer' requirements.txt > /tmp/aigateway-req.txt + pip install -r /tmp/aigateway-req.txt + pip install --no-deps 'presidio-anonymizer>=2.2.364' - name: Start AIGateway working-directory: AIGateway/src From 1ae2946f7d4d696d7680c456e55f7d9b4c813479 Mon Sep 17 00:00:00 2001 From: Inna Glamazda Date: Tue, 11 Aug 2026 20:02:48 -0400 Subject: [PATCH 8/8] Revert "ci(e2e): work around AIGateway cryptography/presidio install conflict" This reverts commit 2a10d014780baecfe683032c70f3266cc1d2c543. --- .github/workflows/e2e-tests.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 6973da68c8..c8bf13e7e4 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -91,12 +91,7 @@ jobs: - name: Install AIGateway dependencies working-directory: AIGateway - run: | - # presidio-anonymizer 2.2.364 pins cryptography<49, which conflicts - # with cryptography>=50 (CVE-2026-69247). Install it without that pin. - grep -v '^presidio-anonymizer' requirements.txt > /tmp/aigateway-req.txt - pip install -r /tmp/aigateway-req.txt - pip install --no-deps 'presidio-anonymizer>=2.2.364' + run: pip install -r requirements.txt - name: Start AIGateway working-directory: AIGateway/src