Skip to content

Commit 3afc73c

Browse files
authored
Merge pull request #2968 from bluewave-labs/develop-saas
Merge develop-saas into master-saas
2 parents 51fc24a + c6343ea commit 3afc73c

File tree

56 files changed

+2386
-2234
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+2386
-2234
lines changed

Clients/nginx.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ server {
1717

1818
# Proxy API requests to backend
1919
location /api {
20-
proxy_pass http://localhost:3000; # no trailing slash - keeps /api prefix
20+
proxy_pass http://localhost:3000; # no trailing slash - keeps /api prefix
2121
proxy_http_version 1.1;
2222
proxy_set_header Host $host;
2323
proxy_set_header X-Real-IP $remote_addr;

Clients/src/infrastructure/api/deepEvalDatasetsService.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,34 @@ export function isMultiTurnConversation(record: DatasetPromptRecord): record is
5959
}
6060

6161
class DeepEvalDatasetsService {
62-
async uploadDataset(file: File, datasetType: DatasetType = "chatbot", turnType: TurnType = "single-turn"): Promise<UploadDatasetResponse> {
62+
async uploadDataset(file: File, datasetType: DatasetType = "chatbot", turnType: TurnType = "single-turn", orgId?: string): Promise<UploadDatasetResponse> {
63+
// org_id is required by the backend - fetch current org if not provided
64+
let finalOrgId = orgId;
65+
if (!finalOrgId) {
66+
// Dynamically import to avoid circular dependency
67+
const { deepEvalOrgsService } = await import("./deepEvalOrgsService");
68+
const { org } = await deepEvalOrgsService.getCurrentOrg();
69+
if (org) {
70+
finalOrgId = org.id;
71+
} else {
72+
// Try to get first org
73+
const { orgs } = await deepEvalOrgsService.getAllOrgs();
74+
if (orgs && orgs.length > 0) {
75+
finalOrgId = orgs[0].id;
76+
await deepEvalOrgsService.setCurrentOrg(finalOrgId);
77+
}
78+
}
79+
}
80+
81+
if (!finalOrgId) {
82+
throw new Error("No organization available. Please create an organization first.");
83+
}
84+
6385
const form = new FormData();
6486
form.append("dataset", file);
6587
form.append("dataset_type", datasetType);
6688
form.append("turn_type", turnType);
89+
form.append("org_id", finalOrgId);
6790
const res = await CustomAxios.post("/deepeval/datasets/upload", form, {
6891
headers: { "Content-Type": "multipart/form-data" },
6992
});

Clients/src/infrastructure/api/deepEvalScorersService.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export type ScorerType = "llm" | "builtin" | "custom";
44

55
export interface DeepEvalScorer {
66
id: string;
7-
projectId?: string | null;
7+
orgId?: string | null;
88
name: string;
99
description?: string | null;
1010
type: ScorerType;
@@ -24,13 +24,35 @@ export interface ListScorersResponse {
2424
}
2525

2626
class DeepEvalScorersService {
27-
async list(params?: { project_id?: string }): Promise<ListScorersResponse> {
27+
async list(params?: { org_id?: string }): Promise<ListScorersResponse> {
2828
const res = await CustomAxios.get("/deepeval/scorers", { params });
2929
return res.data as ListScorersResponse;
3030
}
3131

3232
async create(payload: Partial<DeepEvalScorer> & { name: string; metricKey: string }): Promise<DeepEvalScorer> {
33-
const res = await CustomAxios.post("/deepeval/scorers", payload);
33+
// org_id is required by the backend - fetch current org if not provided
34+
let finalOrgId = payload.orgId;
35+
if (!finalOrgId) {
36+
// Dynamically import to avoid circular dependency
37+
const { deepEvalOrgsService } = await import("./deepEvalOrgsService");
38+
const { org } = await deepEvalOrgsService.getCurrentOrg();
39+
if (org) {
40+
finalOrgId = org.id;
41+
} else {
42+
// Try to get first org
43+
const { orgs } = await deepEvalOrgsService.getAllOrgs();
44+
if (orgs && orgs.length > 0) {
45+
finalOrgId = orgs[0].id;
46+
await deepEvalOrgsService.setCurrentOrg(finalOrgId);
47+
}
48+
}
49+
}
50+
51+
if (!finalOrgId) {
52+
throw new Error("No organization available. Please create an organization first.");
53+
}
54+
55+
const res = await CustomAxios.post("/deepeval/scorers", { ...payload, orgId: finalOrgId });
3456
return res.data as DeepEvalScorer;
3557
}
3658

Clients/src/presentation/components/ContextSidebar/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,17 @@ const ContextSidebar: FC<ContextSidebarProps> = ({
3434
const activeTab = location.hash.replace("#", "") || "overview";
3535

3636
// Handle tab change by navigating to the new hash
37+
// If we have a selected project but aren't on a project-specific URL, navigate to the project
3738
const handleTabChange = (newTab: string) => {
38-
navigate(`${location.pathname}#${newTab}`, { replace: true });
39+
const pathParts = location.pathname.split("/");
40+
const hasProjectInUrl = pathParts.length > 2 && pathParts[2]; // /evals/:projectId
41+
42+
// If there's no project in URL but we have a selected project, navigate to that project
43+
if (!hasProjectInUrl && evalsSidebarContext?.currentProject) {
44+
navigate(`/evals/${evalsSidebarContext.currentProject.id}#${newTab}`, { replace: true });
45+
} else {
46+
navigate(`${location.pathname}#${newTab}`, { replace: true });
47+
}
3948
};
4049

4150
switch (activeModule) {

Clients/src/presentation/pages/EvalsDashboard/BuiltInDatasetsPage.tsx

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
22
import { Box, Stack, Typography, Chip, Paper, Divider, Button, CircularProgress, IconButton, Select, MenuItem, useTheme } from "@mui/material";
33
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
44
import { deepEvalDatasetsService, DatasetPromptRecord, isSingleTurnPrompt, SingleTurnPrompt } from "../../../infrastructure/api/deepEvalDatasetsService";
5+
import { deepEvalOrgsService } from "../../../infrastructure/api/deepEvalOrgsService";
56
import { experimentsService } from "../../../infrastructure/api/evaluationLogsService";
67
import Alert from "../../components/Alert";
78
import { ArrowLeft, X, Settings, ChevronDown, Upload } from "lucide-react";
@@ -151,6 +152,19 @@ export default function BuiltInDatasetsPage(_props: BuiltInEmbedProps) {
151152
const [uploading, setUploading] = useState(false);
152153
const fileInputRef = useRef<HTMLInputElement | null>(null);
153154
const [activeSection, setActiveSection] = useState<"datasets" | "benchmarks">("datasets");
155+
const [orgId, setOrgId] = useState<string | null>(null);
156+
157+
// Fetch current org on mount
158+
useEffect(() => {
159+
(async () => {
160+
try {
161+
const { org } = await deepEvalOrgsService.getCurrentOrg();
162+
if (org) setOrgId(org.id);
163+
} catch {
164+
// Ignore - org might not be set
165+
}
166+
})();
167+
}, []);
154168

155169
useEffect(() => {
156170
(async () => {
@@ -233,7 +247,7 @@ export default function BuiltInDatasetsPage(_props: BuiltInEmbedProps) {
233247

234248
try {
235249
setUploading(true);
236-
const resp = await deepEvalDatasetsService.uploadDataset(file);
250+
const resp = await deepEvalDatasetsService.uploadDataset(file, "chatbot", "single-turn", orgId || undefined);
237251
setAlert({ variant: "success", body: `Uploaded ${resp.filename}` });
238252
setTimeout(() => setAlert(null), 4000);
239253
// Reload groups so any new datasets that are exposed via list() appear
@@ -680,43 +694,43 @@ export default function BuiltInDatasetsPage(_props: BuiltInEmbedProps) {
680694
const stp = p as SingleTurnPrompt;
681695
return (
682696
<Paper key={stp.id} variant="outlined" sx={{ p: 1.25 }}>
683-
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 0.5 }}>
684-
<Typography sx={{ fontWeight: 700, fontSize: "12px" }}>{`Prompt ${idx + 1}`}</Typography>
685-
<Chip
697+
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 0.5 }}>
698+
<Typography sx={{ fontWeight: 700, fontSize: "12px" }}>{`Prompt ${idx + 1}`}</Typography>
699+
<Chip
686700
label={stp.category}
687-
size="small"
688-
sx={{
689-
height: 18,
690-
fontSize: "10px",
701+
size="small"
702+
sx={{
703+
height: 18,
704+
fontSize: "10px",
691705
bgcolor: stp.category?.toLowerCase().includes("coding")
692-
? "#E6F4EF"
706+
? "#E6F4EF"
693707
: stp.category?.toLowerCase().includes("math")
694-
? "#E6F1FF"
708+
? "#E6F1FF"
695709
: stp.category?.toLowerCase().includes("reason")
696-
? "#FFF4E6"
697-
: "#F3F4F6",
698-
}}
699-
/>
700-
</Stack>
701-
<Typography sx={{ fontSize: "12px", color: "#111827", whiteSpace: "pre-wrap" }}>
710+
? "#FFF4E6"
711+
: "#F3F4F6",
712+
}}
713+
/>
714+
</Stack>
715+
<Typography sx={{ fontSize: "12px", color: "#111827", whiteSpace: "pre-wrap" }}>
702716
{stp.prompt}
703-
</Typography>
717+
</Typography>
704718
{stp.expected_output && (
705-
<Typography sx={{ mt: 0.75, fontSize: "12px", color: "#4B5563" }}>
719+
<Typography sx={{ mt: 0.75, fontSize: "12px", color: "#4B5563" }}>
706720
<b>Expected:</b> {stp.expected_output}
707-
</Typography>
708-
)}
721+
</Typography>
722+
)}
709723
{Array.isArray(stp.expected_keywords) && stp.expected_keywords.length > 0 && (
710-
<Typography sx={{ mt: 0.5, fontSize: "12px", color: "#4B5563" }}>
724+
<Typography sx={{ mt: 0.5, fontSize: "12px", color: "#4B5563" }}>
711725
<b>Keywords:</b> {stp.expected_keywords.join(", ")}
712-
</Typography>
713-
)}
726+
</Typography>
727+
)}
714728
{Array.isArray(stp.retrieval_context) && stp.retrieval_context.length > 0 && (
715-
<Typography sx={{ mt: 0.5, fontSize: "12px", color: "#4B5563", whiteSpace: "pre-wrap" }}>
729+
<Typography sx={{ mt: 0.5, fontSize: "12px", color: "#4B5563", whiteSpace: "pre-wrap" }}>
716730
<b>Context:</b> {stp.retrieval_context.join("\n")}
717-
</Typography>
718-
)}
719-
</Paper>
731+
</Typography>
732+
)}
733+
</Paper>
720734
);
721735
} else {
722736
// Multi-turn conversation

Clients/src/presentation/pages/EvalsDashboard/CreateScorerModal.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ export default function CreateScorerModal({
166166

167167
// Update config when initialConfig changes (for editing)
168168
useEffect(() => {
169+
// Always reset popover state when modal opens or scorer changes
170+
setParamsPopoverOpen(false);
171+
169172
if (initialConfig) {
170173
setConfig({
171174
name: initialConfig.name || "",
@@ -408,11 +411,11 @@ export default function CreateScorerModal({
408411
</Typography>
409412
<CustomizableButton
410413
variant="text"
411-
text="Add API key in Configuration"
414+
text="Add API key in Settings"
412415
icon={<Settings size={14} />}
413416
onClick={() => {
414417
onClose();
415-
navigate(`/evals/${projectId}#configuration`)
418+
navigate(`/evals/${projectId}#settings`)
416419
}}
417420
sx={{
418421
color: "#92400E",

Clients/src/presentation/pages/EvalsDashboard/DatasetEditorPage.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "@mui/material";
1616
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
1717
import { deepEvalDatasetsService, DatasetPromptRecord, SingleTurnPrompt, isSingleTurnPrompt } from "../../../infrastructure/api/deepEvalDatasetsService";
18+
import { deepEvalOrgsService } from "../../../infrastructure/api/deepEvalOrgsService";
1819
import Alert from "../../components/Alert";
1920
import { ArrowLeft, ChevronDown, Save as SaveIcon, Plus, Trash2, Download, Copy, Check } from "lucide-react";
2021
import PageBreadcrumbs from "../../components/Breadcrumbs/PageBreadcrumbs";
@@ -30,6 +31,19 @@ export default function DatasetEditorPage() {
3031
const [saving, setSaving] = useState<boolean>(false);
3132
const [copied, setCopied] = useState<boolean>(false);
3233
const [alert, setAlert] = useState<{ variant: "success" | "error"; body: string } | null>(null);
34+
const [orgId, setOrgId] = useState<string | null>(null);
35+
36+
// Fetch current org on mount
37+
useEffect(() => {
38+
(async () => {
39+
try {
40+
const { org } = await deepEvalOrgsService.getCurrentOrg();
41+
if (org) setOrgId(org.id);
42+
} catch {
43+
// Ignore - org might not be set
44+
}
45+
})();
46+
}, []);
3347

3448
useEffect(() => {
3549
(async () => {
@@ -63,7 +77,7 @@ export default function DatasetEditorPage() {
6377
const slug = datasetName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
6478
const finalName = slug ? `${slug}.json` : "dataset.json";
6579
const file = new File([blob], finalName, { type: "application/json" });
66-
await deepEvalDatasetsService.uploadDataset(file);
80+
await deepEvalDatasetsService.uploadDataset(file, "chatbot", "single-turn", orgId || undefined);
6781
setAlert({ variant: "success", body: `Dataset "${datasetName}" saved successfully!` });
6882
setTimeout(() => {
6983
setAlert(null);
@@ -193,7 +207,7 @@ export default function DatasetEditorPage() {
193207
startIcon={<SaveIcon size={16} />}
194208
onClick={handleSave}
195209
>
196-
{saving ? "Saving..." : "Save copy"}
210+
{saving ? "Saving..." : "Save"}
197211
</Button>
198212
</Stack>
199213
</Stack>
@@ -209,7 +223,7 @@ export default function DatasetEditorPage() {
209223
placeholder="Enter a descriptive name for this dataset"
210224
/>
211225
<Typography variant="body2" sx={{ color: "#6B7280", fontSize: "13px" }}>
212-
Edit the prompts below, then click Save to add a copy to your datasets.
226+
Edit the prompts below, then click Save to create your dataset.
213227
</Typography>
214228
</Stack>
215229

0 commit comments

Comments
 (0)