Skip to content

Commit

Permalink
[DRAFT] public experiments (#920)
Browse files Browse the repository at this point in the history
  • Loading branch information
acashmoney authored Mar 20, 2024
1 parent e7ac101 commit 393d831
Show file tree
Hide file tree
Showing 31 changed files with 433 additions and 24 deletions.
11 changes: 9 additions & 2 deletions frontend/app/data/AddDataFileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

import { usePrivy } from "@privy-io/react-auth";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";

import { Alert } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
endFileUploadDataSlice,
fetchUserDataAsync,
saveDataFileAsync,
selectDataFileError,
selectDataFileIsLoading,
selectUserIsAdmin,
setError,
startFileUploadDataSlice,
useDispatch,
Expand All @@ -32,6 +34,7 @@ export default function AddDataFileForm({ trigger }: AddDataFileFormProps) {
const errorMessage = useSelector(selectDataFileError);
const isLoading = useSelector(selectDataFileIsLoading);
const walletAddress = user?.wallet?.address;
const isAdmin = useSelector(selectUserIsAdmin);

const [file, setFile] = useState<File | null>(null);
const [isPublic, setIsPublic] = useState(false);
Expand Down Expand Up @@ -72,6 +75,10 @@ export default function AddDataFileForm({ trigger }: AddDataFileFormProps) {
}
};

useEffect(() => {
dispatch(fetchUserDataAsync());
}, [dispatch]);

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
Expand All @@ -81,7 +88,7 @@ export default function AddDataFileForm({ trigger }: AddDataFileFormProps) {
<DialogDescription>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<Input type="file" onChange={handleFileChange} />
{file && (
{isAdmin && file && (
<label>
<input
type="checkbox"
Expand Down
69 changes: 64 additions & 5 deletions frontend/app/experiments/ExperimentDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { BadgeCheck, Dna, Share2 } from "lucide-react";
import Link from "next/link";
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";

import { CopyToClipboard } from "@/components/shared/CopyToClipboard";
Expand All @@ -14,8 +15,10 @@ import { Alert } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { AppDispatch, flowDetailThunk, selectFlowDetail, selectFlowDetailError, selectFlowDetailLoading } from "@/lib/redux";
import { AppDispatch, flowDetailThunk, selectFlowDetail, selectFlowDetailError, selectFlowDetailLoading, selectFlowUpdateError, selectFlowUpdateLoading, selectFlowUpdateSuccess, selectUserWalletAddress, setFlowDetailPublic } from "@/lib/redux";
import { flowUpdateThunk } from "@/lib/redux/slices/flowUpdateSlice/thunks";

import ExperimentShare from "./ExperimentShare";
import { aggregateJobStatus, ExperimentStatus } from "./ExperimentStatus";
import JobDetail from "./JobDetail";

Expand All @@ -29,6 +32,29 @@ export default function ExperimentDetail({ experimentID }: { experimentID: strin

const status = aggregateJobStatus(flow.Jobs);

const [isDelaying, setIsDelaying] = useState(false);
const updateLoading = useSelector(selectFlowUpdateLoading);
const updateError = useSelector(selectFlowUpdateError);
const updateSuccess = useSelector(selectFlowUpdateSuccess);

const userWalletAddress = useSelector(selectUserWalletAddress);

const handlePublish = () => {
setIsDelaying(true);
dispatch(flowUpdateThunk({ flowId: experimentID }))
.then(() => {
setTimeout(() => {
dispatch(setFlowDetailPublic(true));
setIsDelaying(false);
}, 2000);
})
setTimeout(() => {
setIsDelaying(false);
}, 2000);
};

const isButtonDisabled = updateLoading || isDelaying;

useEffect(() => {
if (experimentID) {
dispatch(flowDetailThunk(experimentID));
Expand Down Expand Up @@ -58,9 +84,42 @@ export default function ExperimentDetail({ experimentID }: { experimentID: strin
<Card>
<CardContent className="pb-0">
{error && <Alert variant="destructive">{error}</Alert>}
<div className="flex text-xl">
<ExperimentStatus jobs={flow.Jobs} className="mr-2 mt-2.5" />
<span className="font-heading">{flow.Name}</span>
<div className="flex items-center justify-between">
<div className="flex text-xl">
<ExperimentStatus jobs={flow.Jobs} className="mr-2 mt-2.5" />
<span className="font-heading">{flow.Name}</span>
</div>
<div className="flex justify-end space-x-2 mt-4">
{userWalletAddress === flow.WalletAddress && (
<Button
variant="outline"
className="text-sm"
onClick={handlePublish}
disabled={updateLoading || flow.Public}
>
{updateLoading || isDelaying ? (
<>
<Dna className="animate-spin w-4 h-4 ml-2" />
<span>Publishing...</span>
</>
) : flow.Public? (
<>
<BadgeCheck className="w-4 h-4 mr-2" /> Published
</>
) : (
<>
<Dna className="w-4 h-4 mr-2" /> Publish
</>
)}
</Button>
)}
{flow.Public && (
// <Button variant="outline" className="text-sm">
// <Share2 className="w-4 h-4 mr-2" /> Share
// </Button>
<ExperimentShare experimentID={experimentID} />
)}
</div>
</div>
<div className="py-4 pl-5 space-y-1 text-xs">
<div className="opacity-70">
Expand Down
33 changes: 33 additions & 0 deletions frontend/app/experiments/ExperimentShare.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Share2 } from 'lucide-react';
import React, { useState } from 'react';

import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";

const ExperimentShare = ({ experimentID }: { experimentID: string }) => {
const [copied, setCopied] = useState(false);
const currentPageLink = `${process.env.NEXT_PUBLIC_FRONTEND_URL}/experiments/${experimentID}`;

const copyLinkToClipboard = async () => {
await navigator.clipboard.writeText(currentPageLink);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 2000);
};

return (
<TooltipProvider>
<Tooltip open={copied}>
<TooltipTrigger asChild>
<Button variant="outline" className="text-sm" onClick={copyLinkToClipboard}>
<Share2 className="w-4 h-4 mr-2" /> Share
</Button>
</TooltipTrigger>
<TooltipContent side="right">Copied!</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};

export default ExperimentShare;
7 changes: 6 additions & 1 deletion frontend/app/experiments/MetricsVisualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@ export default function MetricsVisualizer({ job }: { job: JobDetail }) {
return (
<div className="p-4 text-center text-muted-foreground">
<CircleDotDashedIcon size={32} className={cn(job.State === "running" && "animate-spin", "mx-auto my-4")} absoluteStrokeWidth />
<p>No checkpoints found yet. If this model has checkpoints, they&apos;ll appear here as they are reached.</p>
<p>
No checkpoints found yet. While waiting, check out a completed, public experiment.
</p>
<p>
<a href={process.env.NEXT_PUBLIC_DEMO_URL} className="font-bold hover:underline" target="_blank" rel="noopenner noreferrer">Results</a>
</p>
</div>
);
}
Expand Down
9 changes: 8 additions & 1 deletion frontend/app/tasks/[slug]/[[...toolCID]]/ModelInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { BookOpenIcon, FileJsonIcon, GithubIcon } from "lucide-react";
import { BookOpenIcon, FileJsonIcon, FileLineChart, GithubIcon } from "lucide-react";
import React from "react";

import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -49,6 +49,13 @@ export default function ModelInfo({ tool }: ModelInfoProps) {
return (
<>
{renderDescriptionParagraphs(description)}
<div className="flex gap-2 mt-4">
<Button asChild variant="outline" size="xs">
<a href={process.env.NEXT_PUBLIC_DEMO_URL} target="_blank">
<FileLineChart />Example Result
</a>
</Button>
</div>
<div className="flex gap-2 mt-4">
{github && (
<Button asChild variant="outline" size="xs">
Expand Down
4 changes: 3 additions & 1 deletion frontend/components/global/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useDispatch, useSelector } from "react-redux";
import { ExperimentStatus } from "@/app/experiments/ExperimentStatus";
import { NavButton } from "@/components/global/NavItem";
import { ScrollArea } from "@/components/ui/scroll-area";
import { AppDispatch, Flow, flowListThunk, selectCategorizedFlows, selectFlowList, selectFlowListLoading } from "@/lib/redux";
import { AppDispatch, Flow, flowListThunk, selectCategorizedFlows, selectFlowList, selectFlowListLoading, selectUserIsAdmin } from "@/lib/redux";

import Logo from "./Logo";
import { NavLink } from "./NavItem";
Expand All @@ -28,6 +28,7 @@ export default function Nav() {
const flows = useSelector(selectFlowList);
// const loading = useSelector(selectFlowListLoading);
const walletAddress = user?.wallet?.address;
const isAdmin = useSelector(selectUserIsAdmin);

useEffect(() => {
console.log("walletAddress", walletAddress);
Expand All @@ -42,6 +43,7 @@ export default function Nav() {
<Link href="/" className="flex items-center h-12 gap-2 p-2 text-lg font-bold uppercase font-heading whitespace-nowrap">
<Logo className="w-auto h-6 text-primary" />
Lab.Bio
{isAdmin && <sup className="text-xs text-primary">Admin</sup>}
</Link>
<NavContent>
<NavLink
Expand Down
2 changes: 2 additions & 0 deletions frontend/lib/redux/rootReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
flowAddSlice,
flowDetailSlice,
flowListSlice,
flowUpdateSlice,
jobDetailSlice,
stripeCheckoutSlice,
toolAddSlice,
Expand All @@ -26,6 +27,7 @@ export const reducer = {
flowAdd: flowAddSlice.reducer,
flowList: flowListSlice.reducer,
flowDetail: flowDetailSlice.reducer,
flowUpdate: flowUpdateSlice.reducer,
jobDetail: jobDetailSlice.reducer,
apiKeyAdd: apiKeyAddSlice.reducer,
apiKeyList: apiKeyListSlice.reducer,
Expand Down
8 changes: 6 additions & 2 deletions frontend/lib/redux/slices/flowDetailSlice/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface FlowDetail {
WalletAddress: string;
StartTime: string;
EndTime: string;
Public: boolean;
}

interface FlowDetailSliceState {
Expand All @@ -29,7 +30,7 @@ interface FlowDetailSliceState {
}

const initialState: FlowDetailSliceState = {
flow: { ID: null, CID: "", Jobs: [], Name: "", WalletAddress: "", StartTime: "", EndTime: "" },
flow: { ID: null, CID: "", Jobs: [], Name: "", WalletAddress: "", StartTime: "", EndTime: "", Public: false },
loading: true,
error: null,
success: false,
Expand All @@ -51,9 +52,12 @@ export const flowDetailSlice = createSlice({
setFlowDetailSuccess: (state, action: PayloadAction<boolean>) => {
state.success = action.payload;
},
setFlowDetailPublic: (state, action: PayloadAction<boolean>) => {
state.flow.Public = action.payload;
}
},
});

export const { setFlowDetail, setFlowDetailLoading, setFlowDetailError, setFlowDetailSuccess } = flowDetailSlice.actions;
export const { setFlowDetail, setFlowDetailLoading, setFlowDetailError, setFlowDetailPublic, setFlowDetailSuccess } = flowDetailSlice.actions;

export default flowDetailSlice.reducer;
1 change: 1 addition & 0 deletions frontend/lib/redux/slices/flowListSlice/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface Flow {
WalletAddress: string
Name: string
StartTime: string
Public: boolean
}

export interface CategorizedFlows {
Expand Down
33 changes: 33 additions & 0 deletions frontend/lib/redux/slices/flowUpdateSlice/asyncActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getAccessToken } from "@privy-io/react-auth";
import backendUrl from "lib/backendUrl";

export const updateFlow = async (flowId: string): Promise<any> => {
let authToken;
try {
authToken = await getAccessToken()
} catch (error) {
console.log('Failed to get access token: ', error)
throw new Error("Authentication failed");
}

const requestUrl = `${backendUrl()}/flows/${flowId}`;
const requestOptions = {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json',
},
};

try {
const response = await fetch(requestUrl, requestOptions);
if (!response.ok) {
throw new Error(`Failed to update Flow: ${response.statusText}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error('Failed to update Flow:', error);
throw new Error('Failed to update Flow');
}
};
3 changes: 3 additions & 0 deletions frontend/lib/redux/slices/flowUpdateSlice/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './selectors'
export * from './slice'
export * from './thunks'
5 changes: 5 additions & 0 deletions frontend/lib/redux/slices/flowUpdateSlice/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { ReduxState } from '@/lib/redux'

export const selectFlowUpdateLoading = (state: ReduxState) => state.flowUpdate.loading;
export const selectFlowUpdateError = (state: ReduxState) => state.flowUpdate.error;
export const selectFlowUpdateSuccess = (state: ReduxState) => state.flowUpdate.success;
37 changes: 37 additions & 0 deletions frontend/lib/redux/slices/flowUpdateSlice/slice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

export interface FlowUpdateSliceState {
loading: boolean;
error: string | null;
success: boolean;
}

const initialState: FlowUpdateSliceState = {
loading: false,
error: null,
success: false,
};

export const flowUpdateSlice = createSlice({
name: "FlowUpdate",
initialState,
reducers: {
setFlowUpdateLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setFlowUpdateError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
},
setFlowUpdateSuccess: (state, action: PayloadAction<boolean>) => {
state.success = action.payload;
},
},
});

export const {
setFlowUpdateLoading,
setFlowUpdateError,
setFlowUpdateSuccess,
} = flowUpdateSlice.actions;

export default flowUpdateSlice.reducer;
Loading

0 comments on commit 393d831

Please sign in to comment.