UI refresh, more complete features

This commit is contained in:
catthu
2024-07-17 19:27:33 +07:00
parent 792e741615
commit 912f3f488e
25 changed files with 1890 additions and 541 deletions

View File

@@ -1,14 +1,16 @@
import { ButtonHTMLAttributes, ReactNode } from "react"
const PRIMARY_STYLES = "px-6 py-2 text-sm text-gray-200 bg-blue-700 rounded focus:outline-none focus:bg-blue-700 hover:bg-blue-600"
const PRIMARY_STYLES = "px-6 py-2 text-sm text-gray-200 bg-indigo-700 rounded focus:outline-none focus:bg-indigo-700 hover:bg-indigo-600"
const SECONDARY_STYLES = "px-6 py-2 text-sm text-gray-700 bg-gray-200 rounded focus:outline-none focus:bg-gray-200 hover:bg-gray-400 hover:text-gray-100"
const TERTIARY_STYLES ="px-6 py-2 text-sm text-blue-700 underline focus:outline-none hover:rounded hover:bg-gray-100"
const QUARTERY_STYLES = "px-6 py-2 text-sm text-indigo-700 border rounded border-indigo-700 hover:rounded hover:bg-indigo-300"
const DISABLED_STYLES = "px-6 py-2 text-sm text-gray-200 bg-gray-400 rounded focus:outline-none"
export enum ButtonVariant {
PRIMARY = 'primary',
SECONDARY = 'secondary',
TERTIARY = 'tertiary',
QUARTERY = 'quartery',
DISABLED = 'disabled',
}
@@ -23,6 +25,7 @@ const Button = ({children, variant = ButtonVariant.PRIMARY, ...props}: ButtonPro
case ButtonVariant.PRIMARY: return PRIMARY_STYLES
case ButtonVariant.SECONDARY: return SECONDARY_STYLES
case ButtonVariant.TERTIARY: return TERTIARY_STYLES
case ButtonVariant.QUARTERY: return QUARTERY_STYLES
case ButtonVariant.DISABLED: return DISABLED_STYLES
}
return PRIMARY_STYLES;

View File

@@ -116,19 +116,23 @@ type FormFileProps = {
errors?: any; //TODO
errorMessage?: string;
onChange?: (fileChangeEvent: ChangeEvent<HTMLInputElement>) => void;
fileName?: string;
children?: ReactNode;
} & InputHTMLAttributes<HTMLInputElement>;
export const FormFile = React.forwardRef<HTMLInputElement, FormFileProps>(({errors, errorMessage, onChange, ...props}: FormFileProps, ref) => {
export const FormFile = React.forwardRef<HTMLInputElement, FormFileProps>(({errors, errorMessage, onChange, fileName, children, ...props}: FormFileProps, ref) => {
return (
<div>
<input
type="file"
ref={ref}
className=""
className={`${children ? 'hidden': ''}`}
{...props}
onChange={onChange}
/>
{children && children}
{fileName && <span className="mt-2 text-sm text-gray-600">{fileName}</span>}
</div>
)
})

View File

@@ -1,14 +1,23 @@
import { Breadcrumb } from "@/types/request";
import Link from "next/link";
type HeroProps = {
header: string;
breadcrumb?: Array<Breadcrumb>;
header: React.ReactNode;
subheader?: string;
action?: React.ReactNode
}
const Hero = (props: HeroProps) => {
const { header, subheader, action } = props;
const { header, subheader, action, breadcrumb } = props;
return (
<div className="bg-blue-100 px-4 py-16 md:py-24 lg:py-32 text-center">
<h1 className="text-6xl md:text-8xl font-bold text-blue-800 font-inter mb-6">
<div className="px-24 py-16 md:py-24 lg:py-32 text-start">
{breadcrumb?.length &&
<div className="mb-8 text-sm text-slate-900">
{breadcrumb.map((path: Breadcrumb) => <Link key={path.label} href={path.href}>{ path.label }</Link>)}
</div>
}
<h1 className="text-6xl md:text-8xl font-bold text-indigo-900 font-inter mb-6">
{ header }
</h1>
<p className="text-xl md:text-2xl text-gray-700 mb-8 font-inter">
@@ -19,4 +28,24 @@ const Hero = (props: HeroProps) => {
)
}
export default Hero;
export const SmallHero = (props: HeroProps) => {
const { breadcrumb, header, subheader } = props;
// TODO ADD BREADCRUMB
return (
<div className="px-24 py-8 md:py-8 lg:py-8 text-start">
{breadcrumb?.length &&
<div className="mb-8 text-sm text-slate-900">
{breadcrumb.map((path: Breadcrumb) => <Link key={path.label} href={path.href}> { path.label } &gt;</Link>)}
</div>
}
<h1 className="text-3xl md:text-3xl font-bold text-indigo-900 font-inter mb-2">
{ header }
</h1>
<p className="text-md md:text-xl text-gray-700 mb-4 font-inter">
{ subheader }
</p>
</div>
)
}
export default Hero;

View File

@@ -12,11 +12,15 @@ type ModalProps = {
const Modal = (props: ModalProps) => {
const { children, title, onClose } = props;
return (
<div>
<div
className="fixed w-3/4 max-h-screen top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 overflow-scroll z-50"
className="fixed w-full h-full top-0 right-0 opacity-70 bg-gradient-to-r from-slate-600 to-slate-800"
></div>
<div
className="fixed w-1/2 max-h-screen top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 overflow-scroll z-50"
>
<div
className="bg-white border border-blue-200 rounded w-full my-8"
className="bg-white border border-blue-200 rounded-lg w-full my-8"
>
<div
className="fixed right-4 top-10 hover:cursor-pointer"
@@ -29,7 +33,7 @@ const Modal = (props: ModalProps) => {
>
{title &&
<div
className="text-2xl font-bold self-center"
className="text-2xl font-bold self-left"
>
{title}
</div>
@@ -38,6 +42,7 @@ const Modal = (props: ModalProps) => {
</div>
</div>
</div>
</div>
)
}

View File

@@ -2,7 +2,7 @@ import { useUser } from "@/utils/session";
import { faCircle, faUser } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Link from "next/link";
import { useState } from "react";
import { ReactNode, useState } from "react";
const NavBar = () => {
const [ showUserMenu, setShowUserMenu ] = useState<boolean>(false);
@@ -11,17 +11,19 @@ const NavBar = () => {
}
const user = useUser();
return (
<div>
<div className="my-4 px-24 flex justify-between w-full">
<div>
<div
className="m-4 py-2 px-12 rounded bg-slate-800 text-white"
>
<div className="flex justify-between items-center w-full">
<NavBarItem>
<Link href="/">Home</Link>
</div>
</NavBarItem>
<div className="flex gap-4 items-center">
{user && user.username}
<div className="flex items-center hover:cursor-pointer" onClick={onUserMenuClick}>
<span className="fa-layers fa-fw h-8 w-8" style={{ backgroundColor: 'transparent' }}>
<FontAwesomeIcon icon={faCircle} size="2x" className="text-blue-300 mx-auto w-full"/>
<FontAwesomeIcon icon={faUser} className="text-blue-800 mx-auto w-full"/>
<FontAwesomeIcon icon={faCircle} size="2x" className="text-white mx-auto w-full"/>
<FontAwesomeIcon icon={faUser} className="text-gray-800 mx-auto w-full"/>
</span>
</div>
</div>
@@ -33,19 +35,26 @@ const NavBar = () => {
)
}
const NavBarItem = ({ children }: {children: ReactNode}) => {
return (
<div
className="hover:underline"
>
{children}
</div>
)
}
type UserMenuProps = {
authenticated: boolean;
}
const UserMenu = ({ authenticated }: UserMenuProps) => {
return (
<div className="flex flex-col absolute right-0 bg-white px-4 py-4">
<div className="flex flex-col absolute right-0 bg-slate-800 p-2 m-4 rounded">
<UserMenuItem label="Account" path="/" />
<hr />
<UserMenuItem label="Notifications" path="/" />
<hr />
<UserMenuItem label="Add a Dataset" path="/datasets/upload" />
<hr />
{authenticated
? <UserMenuItem label="Sign Out" path="/auth/signout" />
:
@@ -62,7 +71,7 @@ type UserMenuItemProps = {
const UserMenuItem = ({ label, path }: UserMenuItemProps) => {
return (
<Link href={path} className="px-8 py-4">
<Link href={path} className="px-8 py-4 hover:text-slate-800 hover:bg-indigo-50 rounded">
{ label }
</Link>
)

View File

@@ -2,29 +2,29 @@ import { DatasetPreview } from '@/types/dataset';
import Link from 'next/link';
type DatasetCardProps = {
previewImage: React.ReactNode; // todo
dataset: DatasetPreview;
}
export const DatasetCard = (props: DatasetCardProps) => {
const { previewImage, dataset } = props;
const { id, title, description } = dataset;
const { dataset } = props;
const { id, title, description, updatedAt } = dataset;
console.log(dataset)
return (
<Link href={`/datasets/${id}`}>
<div className="transform transition duration-100 ease-in-out hover:scale-105 hover:shadow-lg bg-white rounded-lg overflow-hidden border-2 border-gray-100" >
<div className="bg-gray-300 h-48 w-full flex items-center justify-center">
<div className="transform transition duration-100 ease-in-out hover:scale-105 hover:shadow-lg bg-indigo-100 rounded-lg overflow-hidden" >
{/* <div className="bg-gray-300 h-48 w-full flex items-center justify-center">
Preview Image
{ previewImage }
</div>
</div> */}
<div className="p-4">
<div className="font-semibold text-lg mb-2">
<div className="font-semibold text-lg text-slate-800 mb-2">
{ title }
</div>
<p className="text-gray-600">{ description }</p>
<p className="text-gray-600 text-sm">Uploaded 3 hours ago</p>
<p className="text-gray-600 text-sm">1,204 views · 10 minutes</p>
<p className="text-gray-400 text-xs my-2">Updated on {updatedAt}</p>
</div>
</div>
</Link>

View File

@@ -39,40 +39,53 @@ const NewRequestForm = (props: NewRequestProps) => {
<div>
<FormWrapper onSubmit={handleSubmit(onSubmit)}>
<div
className="bg-gray-50 rounded p-4 mb-4 border-2 border-gray-100"
className="bg-indigo-100 rounded p-4 mb-4"
>
<FormItem>
<div
className="mb-2 text-md font-medium text-gray-900"
className="my-2 text-sm font-medium text-gray-900"
>
Request Name
</div>
<FormInput
errors={errors}
errorMessage={'Just a test error'}
className="bg-indigo-100 border border-indigo-300 rounded w-full text-sm block p-2.5"
placeholder="The title of your computation request"
{...register('title', { required: true })}
/>
</FormItem>
<FormItem>
<div
className="mb-2 text-sm font-medium text-gray-900"
>
Request Description
</div>
<FormTextArea
errors={errors}
errorMessage="text area error"
className="bg-indigo-100 border border-indigo-300 rounded w-full text-sm block p-2.5"
placeholder="Describe what your computation is about"
{...register('description', { required: true })}
/>
</FormItem>
</div>
<div
className="bg-gray-50 rounded p-4 mb-4 border-2 border-gray-100"
className="bg-indigo-100 rounded p-4 mb-4"
>
<FormItem>
<div
className="my-2 text-sm font-medium text-gray-900"
>
Function to Compute
<p className="text-sm">Some help text here about function signature, data types...</p>
<p className="text-xs">Some help text here about function signature, data types...</p>
</div>
{/* TODO SUPPORT TAB TO INDENT */}
<FormTextArea
errors={errors}
errorMessage="text area error"
className="font-mono"
className="bg-indigo-100 border border-indigo-300 rounded w-full text-sm block p-2.5 font-mono"
{...register('code', { required: true })}
>
def computation(s: State, data: list[torch.Tensor]) -&gt; torch.Tensor:&#13;&nbsp;&nbsp;Test

View File

@@ -29,9 +29,6 @@ export const OwnerRequests = (props: OwnerRequestsProps) => {
}
}, [datasetId, setRequests])
const onNewRequestSubmit = () => {
setActiveView(ActiveView.RequestSubmitted)
}
return (
<Layout>

View File

@@ -13,7 +13,11 @@ const ReviewResultModal = (props: ReviewResultModalProps) => {
const { onClose, onAccept, result } = props;
return (
<Modal title="Review Result" onClose={onClose}>
<div
className="text-gray-500"
>
Please review the result below.
</div>
<div className="bg-gray-200 rounded my-4 p-4">
<div className="text-lg font-bold">
<div className="bg-gray-100 rounded text-sm font-light p-2">
@@ -24,14 +28,16 @@ const ReviewResultModal = (props: ReviewResultModalProps) => {
<div
className="flex w-full gap-4"
>
<Button
onClick={onAccept}
>Submit Result</Button>
<Button
variant={ButtonVariant.SECONDARY}
>
Request Further Review
</Button>
<div className="w-full flex justify-between mt-6">
<Button
variant={ButtonVariant.QUARTERY}
>
Request Further Review
</Button>
<Button
onClick={onAccept}
>Approve Result</Button>
</div>
</div>
</Modal>
)

View File

@@ -1,22 +1,20 @@
import Button from "@/components/common/Button";
import Button, { ButtonVariant } from "@/components/common/Button";
import { FormFile, FormInput } from "@/components/common/Form";
import Modal from "@/components/common/Modal";
import { useState } from "react";
type onSubmitResultModalOnSubmit = {
result: string;
proofFile: File |null;
vkFile: File | null;
modelOnnxFile: File | null;
srsFile: File | null;
precalWitnessFile: File | null;
settingsFile: File | null;
}
type SubmitResultModalProps = {
onSubmit: ({
result,
proofFile,
vkFile,
modelOnnxFile,
srsFile,
precalWitnessFile,
settingsFile
}: onSubmitResultModalOnSubmit) => void; // TODO remove the null possibility
onClose: VoidFunction;
@@ -26,36 +24,63 @@ const SubmitResultModal = (props: SubmitResultModalProps) => {
const { onClose, onSubmit } = props;
const [ result, setResult ] = useState<any>();
const [ proofFile, setProofFile ] = useState<File | null>(null);
const [ vkFile, setVkFile ] = useState<File | null>(null);
const [ modelOnnxFile, setModelOnnxFile ] = useState<File | null>(null);
const [ srsFile, setSrsFile ] = useState<File | null>(null);
const [ precalWitnessFile, setPrecalWitnessFile ] = useState<File | null>(null);
const [ settingsFile, setSettingsFile ] = useState<File | null>(null);
// TODO FINISH EXTRACTING THIS
// TODO WHAT HAPPEN WHEN THERE IS MORE THAN ONE RESULT? NEED TO TEST
if (proofFile) {
const reader = new FileReader()
reader.onload = (e) => {
try {
const json = JSON.parse(e.target?.result as string)
console.log(json['pretty_public_inputs']['rescaled_outputs'].slice(1,).join(','))
setResult(json['pretty_public_inputs']['rescaled_outputs'].slice(1,).join(', '))
} catch (error) {
console.error('Error parsing JSON:', error)
}
}
reader.readAsText(proofFile)
}
return (
<Modal title="Submit Result" onClose={onClose}>
Completed the computation? Sumbit the result here.
<FormInput
onChange={(e) => setResult(e.target.value)}
/>
Sumbit the proof here.
<div className="text-gray-500">
Completed the computation? Sumbit the result and proof here.
</div>
<hr />
<h2
className="text-lg font-bold"
>Proof</h2>
<div className="text-gray-500">Select the proof <code>(model.pf)</code> file</div>
<FormFile onChange={(e) => e.target.files && setProofFile(e.target.files[0])}/>
Submit the verification key.
<FormFile onChange={(e) => e.target.files && setVkFile(e.target.files[0])}/>
Submit the model onnx.
<FormFile onChange={(e) => e.target.files && setModelOnnxFile(e.target.files[0])}/>
Submit the SRS file.
<FormFile onChange={(e) => e.target.files && setSrsFile(e.target.files[0])}/>
Submit the settings file.
{result && <p>Result: {result}</p>}
<hr />
<h2
className="text-lg font-bold"
>Precal Witness</h2>
<div className="text-gray-500">Select the precal witness <code>(precal_witness.json)</code> file</div>
<FormFile onChange={(e) => e.target.files && setPrecalWitnessFile(e.target.files[0])}/>
<hr />
<h2
className="text-lg font-bold"
>Settings</h2>
<div className="text-gray-500">Select the settings <code>(settings.json)</code> file</div>
<FormFile onChange={(e) => e.target.files && setSettingsFile(e.target.files[0])}/>
<Button
onClick={() => onSubmit({
proofFile,
vkFile,
modelOnnxFile,
srsFile,
settingsFile
})}
>Submit Result</Button>
<div className="w-full flex justify-end mt-6">
<Button
variant={ButtonVariant.PRIMARY}
onClick={() => onSubmit({
result,
proofFile,
precalWitnessFile,
settingsFile
})}
>Submit Result</Button>
</div>
</Modal>
)
}

View File

@@ -1,16 +1,17 @@
import React, { useEffect, useState, useCallback } from 'react';
import Button, { ButtonVariant } from "@/components/common/Button";
import { RawRequest, RequestPreview, RequestStatus, convertRawRequestToFullRequest } from "@/types/request";
import { APIEndPoints, api } from "@/utils/api";
import Link from "next/link";
import { useEffect, useState } from "react";
import NewRequest from "./NewRequest";
import Layout from "../common/Layout";
import NewRequest from "./NewRequest";
import RequestSubmitted from "./RequestSubmitted";
import { FormInput } from "../common/Form";
type UserRequestsProps = {
userId: string; // TODO uuid
datasetId: string; // TODO
status?: RequestStatus;
userId: string;
datasetId: string;
isDataOwner?: boolean;
}
enum ActiveView {
@@ -19,124 +20,171 @@ enum ActiveView {
RequestSubmitted = 'Request Submitted',
}
export const UserRequests = (props: UserRequestsProps) => {
const { userId, datasetId, status } = props;
const [ requests, setRequests] = useState<Array<RequestPreview>>([]);
const [ activeView, setActiveView ] = useState<ActiveView>(ActiveView.RequestList);
const [ filteredRequests, setFilteredRequests ] = useState<Array<RequestPreview>>([]);
export const UserRequests = ({ userId, datasetId, isDataOwner }: UserRequestsProps) => {
const [requests, setRequests] = useState<RequestPreview[]>([]);
const [activeView, setActiveView] = useState<ActiveView>(ActiveView.RequestList);
const [filteredRequests, setFilteredRequests] = useState<RequestPreview[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (userId && datasetId) {
api(APIEndPoints.Requests, { userId, datasetId}).then((response) => {
setRequests(response.map((request: RawRequest) => convertRawRequestToFullRequest(request)));
setFilteredRequests(response.map((request: RawRequest) => convertRawRequestToFullRequest(request)));
})
}
}, [userId, datasetId, setRequests])
const fetchRequests = async () => {
try {
setIsLoading(true);
const response = await api(APIEndPoints.Requests, {
userId: isDataOwner ? undefined : userId,
datasetId });
const fullRequests = response.map((request: RawRequest) => convertRawRequestToFullRequest(request));
setRequests(fullRequests);
setFilteredRequests(fullRequests);
} catch (err) {
setError('Failed to fetch requests. Please try again later.');
} finally {
setIsLoading(false);
}
};
fetchRequests();
}, [userId, datasetId]);
const onNewRequestSubmit = () => {
setActiveView(ActiveView.RequestSubmitted)
}
const handleNewRequestSubmit = useCallback(() => {
setActiveView(ActiveView.RequestSubmitted);
}, []);
const handleSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const searchTerm = e.target.value.toLowerCase();
setFilteredRequests(requests.filter(request =>
request.title.toLowerCase().includes(searchTerm) ||
request.description?.toLowerCase().includes(searchTerm)
));
}, [requests]);
const activeRequests = requests.filter(request => !request.isCompleted);
const processingRequests = requests.filter(request => request.isAccepted && !request.isCompleted);
const completedRequests = requests.filter(request => request.isCompleted);
return (
<Layout>
<RequestActionBar
<RequestActionBar
isDataOwner={isDataOwner}
numberActive={activeRequests.length}
numberProcessing={processingRequests.length}
numberCompleted={completedRequests.length}
onClickNewRequest={() => setActiveView(ActiveView.NewRequest)}
onClickAll={() => {
onClickActive={() => {
setActiveView(ActiveView.RequestList);
setFilteredRequests(requests);
setFilteredRequests(activeRequests);
}}
onClickProcessing={() => setFilteredRequests(requests.filter(request => request.isAccepted && !request.isCompleted))}
onClickCompleted={() => setFilteredRequests(requests.filter(request => request.isCompleted))}
onClickProcessing={() => setFilteredRequests(processingRequests)}
onClickCompleted={() => setFilteredRequests(completedRequests)}
onSearch={handleSearch}
/>
{activeView === ActiveView.RequestList &&
<div>
{filteredRequests?.map((request) => <RequestRow key={request.id} {...request}/>)}
{isLoading && <div className="text-center my-6">Loading...</div>}
{error && <div className="text-center my-6 text-red-500">{error}</div>}
{!isLoading && !error && activeView === ActiveView.RequestList && (
<div className="space-y-4">
{filteredRequests.map((request) => (
<RequestRow key={request.id} {...request} />
))}
</div>
}
{activeView === ActiveView.NewRequest && <NewRequest userId={userId} datasetId={datasetId} onSubmit={onNewRequestSubmit}/>}
)}
{activeView === ActiveView.NewRequest && (
<NewRequest userId={userId} datasetId={datasetId} onSubmit={handleNewRequestSubmit} />
)}
{activeView === ActiveView.RequestSubmitted && <RequestSubmitted />}
</Layout>
)
}
);
};
type RequestActionBarProps = {
isDataOwner?: boolean;
numberActive: number;
numberProcessing: number;
numberCompleted: number;
onClickNewRequest: VoidFunction;
onClickProcessing:VoidFunction;
onClickActive: VoidFunction;
onClickProcessing: VoidFunction;
onClickCompleted: VoidFunction;
onClickAll: VoidFunction;
onSearch: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const RequestActionBar = (props: RequestActionBarProps) => {
const { onClickNewRequest, onClickCompleted, onClickProcessing, onClickAll } = props;
const RequestActionBar = ({
isDataOwner,
numberActive,
numberProcessing,
numberCompleted,
onClickNewRequest,
onClickActive,
onClickProcessing,
onClickCompleted,
onSearch
}: RequestActionBarProps) => {
return (
<div className="flex space-around gap-2 my-6">
<Button
onClick={onClickNewRequest}
variant={ButtonVariant.SECONDARY}
>
<div className="flex flex-wrap gap-4 my-6 items-center bg-indigo-200 p-4 rounded-lg shadow">
{(isDataOwner !== undefined && !isDataOwner) &&
<Button onClick={onClickNewRequest} variant={ButtonVariant.PRIMARY}>
+ New Request
</Button>
<Button
variant={ButtonVariant.TERTIARY}
onClick={onClickAll}
>
All
}
<Button variant={ButtonVariant.QUARTERY} onClick={onClickActive}>
Active ({numberActive})
</Button>
<Button
variant={ButtonVariant.TERTIARY}
onClick={onClickProcessing}
>
Processing
<Button variant={ButtonVariant.QUARTERY} onClick={onClickProcessing}>
Processing ({numberProcessing})
</Button>
<Button
variant={ButtonVariant.TERTIARY}
onClick={onClickCompleted}
>
Completed
<Button variant={ButtonVariant.QUARTERY} onClick={onClickCompleted}>
Completed ({numberCompleted})
</Button>
<FormInput
placeholder="Search requests..."
onChange={onSearch}
className="flex-grow p-2 rounded border border-indigo-400 bg-indigo-200"
/>
</div>
)
}
);
};
const RequestRow = (props: RequestPreview) => {
const { title, description, datasetId, id, isAccepted, isCompleted } = props;
const RequestRow = ({
title,
description,
datasetId,
id,
isAccepted,
isCompleted,
resultApproved
}: RequestPreview) => {
let statusColor = 'bg-gray-400';
let statusText = 'Awaiting Confirmation'
if (isAccepted) {
let statusText = 'Awaiting Confirmation';
if (isAccepted) {
statusColor = 'bg-yellow-400';
statusText = 'Confirmed, Awaiting Results';
}
if (isCompleted) {
statusColor = 'bg-green-400';
statusText = 'Results Ready'
statusText = 'Results Ready';
}
if (resultApproved) {
statusColor = 'bg-green-800';
statusText = 'Request Completed';
}
return (
<div className="bg-blue-100 shadow-md rounded-lg p-4 mb-4 transition duration-150 ease-in-out hover:shadow-xl hover:scale-105">
<Link href={`/datasets/${datasetId}/requests/${id}`}>
<div
className="flex gap-2 items-center"
>
<div
className="flex items-center gap-2"
>
<h3
className="text-lg font-semibold text-gray-800"
>{ title }</h3>
<div
className={`text-sm ${statusColor} rounded px-2 text-white`}
>
{statusText}
</div>
</div>
<div className="bg-gray-50 border-1 border-gray-200 hover:bg-indigo-100 rounded-lg p-4 mb-2">
<Link href={`/datasets/${datasetId}/requests/${id}`}>
<div className="flex flex-col sm:flex-row gap-4 items-center">
<div className="flex items-center gap-4 w-full">
<div className={`text-xs ${statusColor} rounded-full px-3 py-1 text-white w-1/6 flex justify-center items-center`}>
{statusText}
</div>
<div
className="text-sm text-gray-600"
>
{ description }
<h3 className="text-lg font-semibold text-gray-900 w-2/6">
{title}
</h3>
<div className="text-sm text-gray-600 w-3/6">
{description}
</div>
</Link>
</div>
</div>
)
}
</Link>
</div>
);
};

View File

@@ -1,7 +1,7 @@
import Hero from "@/components/common/Hero";
import Hero, { SmallHero } from "@/components/common/Hero";
import { APIEndPoints, api } from "@/utils/api";
import { GetServerSideProps } from "next";
import { ReactNode, useState } from "react";
import { ReactNode, useEffect, useRef, useState } from "react";
import { UserRequests } from "@/components/requests/UserRequests";
import Button from "@/components/common/Button";
import { Dataset, convertDatasetResponseToDataset } from "@/types/dataset";
@@ -9,6 +9,10 @@ import Layout from "@/components/common/Layout";
import NavBar from "@/components/common/NavBar";
import { useUser } from "@/utils/session";
import { OwnerRequests } from "@/components/requests/OwnerRequests";
import Link from "next/link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faColumns, faFileAlt, faSquare, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FormFile } from "@/components/common/Form";
export const getServerSideProps: GetServerSideProps = async (context) => {
const { id } = context.params as {
@@ -41,11 +45,10 @@ enum MenuTab {
Schema = 'schema',
Discussions = 'discussions',
Requests = 'requests',
RequestsOwnerView = 'requests_owner_view'
}
const DatasetPage = ({ dataset }: DatasetPageProps) => {
const { id, title, description, testDataUrl, sourceDescription, acknowledgement, ownerId } = dataset;
const { id, title, description, testDataUrl, sourceDescription, acknowledgement, ownerId, schema, rows, columns } = dataset;
const [ activeTab, setActiveTab ] = useState<MenuTab>(MenuTab.About);
const user = useUser();
@@ -55,13 +58,14 @@ const DatasetPage = ({ dataset }: DatasetPageProps) => {
return (
<div>
<NavBar />
<Hero header={ title } subheader={ description }/>
<SmallHero header={ title } subheader={ description }/>
<DatasetMenu onTabChange={setActiveTab} activeTab={activeTab} isDatasetOwner={isOwnedByUser} />
{ activeTab === MenuTab.About && <AboutSection description={description} testDataUrl={testDataUrl} sourceDescription={sourceDescription} acknowledgement={acknowledgement} /> }
{ activeTab === MenuTab.History && "Just testing" }
{ activeTab === MenuTab.Requests && (user?.id ? <UserRequests userId={user?.id} datasetId={id} /> : "Please log in.")}
{ activeTab === MenuTab.RequestsOwnerView && <OwnerRequests datasetId={id} />}
{ activeTab === MenuTab.About && <AboutSection datasetId={id} description={description} testDataUrl={testDataUrl} sourceDescription={sourceDescription} acknowledgement={acknowledgement} isOwnedByUser={isOwnedByUser} rows={rows} columns={columns}/> }
{ activeTab === MenuTab.History && <HistorySection /> }
{ activeTab === MenuTab.Discussions && <DiscussionSection />}
{ activeTab === MenuTab.Requests && (user?.id ? <UserRequests userId={user?.id} datasetId={id} isDataOwner={isOwnedByUser} /> : "Please log in.")}
{ activeTab === MenuTab.Schema && <SchemaSection schema={schema}/>}
</div>
)
@@ -76,12 +80,12 @@ type DatasetMenuProps = {
const DatasetMenu = ({ onTabChange, activeTab, isDatasetOwner }: DatasetMenuProps) => {
return (
<div className="py-4">
<div className="container mx-auto flex justify-center space-x-8">
<div className="container mx-24 flex justify-start border-b-2">
<DatasetMenuItemContainer
onClick={() => onTabChange(MenuTab.About)}
isActive={activeTab === MenuTab.About}
>
About
About this dataset
</DatasetMenuItemContainer>
<DatasetMenuItemContainer
onClick={() => onTabChange(MenuTab.History)}
@@ -101,20 +105,12 @@ const DatasetMenu = ({ onTabChange, activeTab, isDatasetOwner }: DatasetMenuProp
>
Discussions
</DatasetMenuItemContainer>
{!isDatasetOwner
? <DatasetMenuItemContainer
<DatasetMenuItemContainer
onClick={() => onTabChange(MenuTab.Requests)}
isActive={activeTab === MenuTab.Requests}
>
Your Requests
{isDatasetOwner ? 'Active Requests' : 'Your Requests'}
</DatasetMenuItemContainer>
: <DatasetMenuItemContainer
onClick={() => onTabChange(MenuTab.RequestsOwnerView)}
isActive={activeTab === MenuTab.RequestsOwnerView}
>
Active Requests
</DatasetMenuItemContainer>
}
</div>
</div>
)
@@ -128,7 +124,7 @@ type DatasetMenuItemContainerProps = {
const DatasetMenuItemContainer = ({ children, onClick, isActive = false }: DatasetMenuItemContainerProps) => {
return (
<div className={`${isActive && 'bg-blue-100 rounded'} menu-item text-gray-900 hover:font-semibold relative hover:text-gray-900 hover:cursor-pointer transition-all px-2 py-2`}
<div className={`${isActive ? 'text-indigo-700 font-bold border-indigo-700 border-b-2' : 'text-gray-500 hover:text-gray-700'} relative hover:cursor-pointer mr-12 py-4 `}
onClick={onClick}
>
{ children }
@@ -156,30 +152,235 @@ const DatasetSection = (props: DatasetSectionProps) => {
}
type AboutSectionProps = {
datasetId: string;
description: string;
testDataUrl?: string;
sourceDescription?: string;
acknowledgement?: string;
isOwnedByUser?: boolean;
rows?: number;
columns?: number;
}
const AboutSection = (props: AboutSectionProps) => {
const { description, testDataUrl, sourceDescription, acknowledgement } = props;
const { datasetId, description, testDataUrl, sourceDescription, acknowledgement, isOwnedByUser, rows, columns } = props;
return (
<Layout>
<div
className="flex flex-col w-full space-between gap-4"
>
<CryptographicAssets datasetId={datasetId} isOwnedByUser={isOwnedByUser} rows={rows} columns={columns}/>
<div
className="flex flex-col flex-grow"
>
<div>
{ description }
</div>
{ testDataUrl && (
<div className="mx-auto m-4">
<Button onClick={() => window.location.href = testDataUrl}>
Download Test Data
</Button>
</div>
)}
{sourceDescription && <DatasetSection title="Data Sources" content={ sourceDescription } />}
{acknowledgement && <DatasetSection title="Acknowledgment" content={ acknowledgement } />}
</div>
<div>
{ description }
</div>
{ testDataUrl && (
<div className="mx-auto m-4">
<Button onClick={() => window.location.href = testDataUrl}>
Download Test Data
</Button>
</div>
)}
{sourceDescription && <DatasetSection title="Data Sources" content={ sourceDescription } />}
{acknowledgement && <DatasetSection title="Acknowledgment" content={ acknowledgement } />}
</div>
</Layout>
)
}
const DiscussionSection = () => {
return (
<Layout>
<div
className="flex w-full space-between"
>
Discussion sections are coming soon.
</div>
</Layout>
)
}
const HistorySection = () => {
return (
<Layout>
<div
className="flex w-full space-between"
>
History sections are coming soon.
</div>
</Layout>
)
}
type SchemaSectionProps = {
schema?: Record<any, string>;
}
type PropertyRowProps = {
key: string;
type: string;
description: string;
}
const SchemaSection = ({ schema }: SchemaSectionProps) => {
if (!schema) {
return (
<Layout>
The owner of this dataset has not provided a schema.
</Layout>
)
}
const rows: Array<PropertyRowProps> = [];
for (let key in (schema.properties as any)) {
//@ts-ignore
const property = schema.properties[key];
rows.push({
key,
type: property.type,
description: property.description || '',
})
}
const PropertyRow = ({ row } : { row: PropertyRowProps }) => {
return (
<tr key={row.key}>
<td
className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"
>{ row.key }</td>
<td
className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"
>{ row.type }</td>
<td
className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"
>{ row.description }</td>
</tr>
)
}
return (
<Layout>
<div
className="flex flex-col w-full space-between"
>
<div
className="mb-8 flex justify-center"
>
<div>{ schema.title }</div>
<div>{ schema.description }</div>
</div>
<table
className="min-w-full divide-y divide-gray-200 p-4"
>
<thead
className="bg-slate-300 text-indigo-800 font-bold"
>
<tr>
<th
className="px-6 py-3 text-left text-xs text-indigo-500 uppercase tracking-wider font-bold"
>Property</th>
<th
className="px-6 py-3 text-left text-xs text-indigo-500 uppercase tracking-wider font-bold"
>Type</th>
<th
className="px-6 py-3 text-left text-xs text-indigo-500 uppercase tracking-wider font-bold"
>Description</th>
</tr>
</thead>
{rows.map(row => (
<PropertyRow key={row.key} row={row} />
))
}
</table>
</div>
</Layout>
)
}
type CryptographicAssetsProps = {
datasetId: string;
rows?: number;
columns?: number;
isOwnedByUser?: boolean;
}
const CryptographicAssets = ({ datasetId, isOwnedByUser, rows, columns } : CryptographicAssetsProps) => {
const [ doesDataCommitmentExist, setDoesDataCommitmentExist ] = useState<boolean | null>(null);
const [ dataCommitmentFile, setDataCommitmentFile ] = useState<File | null>(null);
const dataCommitmentInputRef = useRef();
useEffect(() => {
api(APIEndPoints.CheckFileExists, {
bucketName: 'proof_assets',
filePath: `${datasetId}`,
fileName: 'data_commitment.json'
}).then(result => setDoesDataCommitmentExist(result))
}, [api])
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target?.files ? event.target.files[0] : null;
if (file) {
setDataCommitmentFile(file)
}
};
const handleUpload = async () => {
if (dataCommitmentFile) {
await api(APIEndPoints.UploadDataCommitment, {datasetId, dataCommitmentFile });
window.location.reload();
}
}
return (
<div className="text-slate-500 my-2">
<div className="flex flex-row gap-10">
{doesDataCommitmentExist === null && 'Loading...'}
{doesDataCommitmentExist === false &&
<div className="flex gap-2 items-center text-red-400">
<FontAwesomeIcon icon={faWarning}/> <span className="text-sm">Missing data commitment</span>
{isOwnedByUser &&
<FormFile
//@ts-ignore
ref={dataCommitmentInputRef}
onChange={handleChange}
fileName={dataCommitmentFile ? dataCommitmentFile.name : undefined}
>
<button
className={`mx-2 px-2 py-1 text-xs text-slate-50 rounded ${dataCommitmentFile ? 'bg-indigo-700' : 'bg-indigo-300'}`}
onClick={() =>
dataCommitmentFile
? handleUpload()
//@ts-ignore
: dataCommitmentInputRef?.current?.click()}
>
{dataCommitmentFile ? 'Upload this file' : 'Choose data commitment file'}
</button>
</FormFile>}
</div>}
{doesDataCommitmentExist === true &&
<div
className="cursor-pointer"
onClick={() => api(APIEndPoints.DownloadDataCommitment, { datasetId })}>
<div className="flex gap-2 items-center">
<FontAwesomeIcon icon={faFileAlt} /> <span className="text-sm">Data Commitment</span>
</div>
</div>
}
<div className="flex gap-2 items-center">
{(rows && columns)
? <><FontAwesomeIcon icon={faColumns} /> <span className="text-sm">{rows} rows x {columns} columns</span></>
: <><FontAwesomeIcon icon={faColumns} className="text-red-400"/> <span className="text-sm text-red-400">Missing number of rows or columns</span></>
}
</div>
</div>
</div>
)
}
export default DatasetPage;

View File

@@ -1,9 +1,9 @@
import Button, { ButtonVariant } from "@/components/common/Button";
import Hero from "@/components/common/Hero";
import Hero, { SmallHero } from "@/components/common/Hero";
import Layout from "@/components/common/Layout";
import NavBar from "@/components/common/NavBar";
import { Dataset, convertDatasetResponseToDataset } from "@/types/dataset";
import { FullRequest, convertRawRequestToFullRequest } from "@/types/request";
import { Breadcrumb, FullRequest, convertRawRequestToFullRequest } from "@/types/request";
import { APIEndPoints, api } from "@/utils/api";
import { useUser } from "@/utils/session";
import { GetServerSideProps } from "next";
@@ -12,7 +12,9 @@ import SubmitResultModal from "../../../../components/requests/SubmitResultModal
import ReviewResultModal from "@/components/requests/ReviewResultModal";
import Link from "next/link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFileAlt } from "@fortawesome/free-solid-svg-icons";
import { faCheckCircle, faCircleCheck, faFileAlt, faWarning } from "@fortawesome/free-solid-svg-icons";
import { extractResult } from "@/utils/ezkl";
import { generateVerifierNotebook } from "@/utils/notebook";
export const getServerSideProps: GetServerSideProps = async (context) => {
const { id, requestId } = context.params as {
@@ -52,7 +54,9 @@ type RequestDetailProps = {
const RequestDetail = (props: RequestDetailProps) => {
const { dataset, request } = props;
const { id, title, description, testDataUrl, sourceDescription, acknowledgement, ownerId } = dataset;
const { isAccepted, isCompleted } = request;
const { isAccepted, isCompleted, resultApproved } = request;
const [ isReviewResultModalOpen, setIsReviewResultModalOpen ] = useState<boolean>(false);
const [ submitResultModalOpen, setSubmitResultModalOpen ] = useState<boolean>(false);
const [statusColor, setStatusColor] = useState<string>("bg-gray-400");
const [statusText, setStatusText] = useState<string>("Awaiting Confirmation")
@@ -61,20 +65,58 @@ const RequestDetail = (props: RequestDetailProps) => {
const isDatasetOwner = ownerId === user?.id;
useEffect(() => {
if (resultApproved) {
setStatusColor('bg-green-800');
setStatusText('Request Completed');
return;
}
if (isCompleted) {
setStatusColor('bg-green-400');
setStatusText('Results Ready');
return;
}
if (isAccepted) {
setStatusColor('bg-yellow-400');
setStatusColor('bg-yellow-400 text-gray-900');
setStatusText('Confirmed, Awaiting Results');
}
}, [isCompleted, isAccepted])
const onVerifierNotebookDownload = async () => {
const notebook = await generateVerifierNotebook(request.code);
const notebookBlob = new Blob([notebook], { type: 'application/x-ipynb+json' });
const url = URL.createObjectURL(notebookBlob);
console.log('verifier notebook gen')
const link = document.createElement('a');
link.href = url;
link.download = 'verifier.ipynb'; // Set the desired file name
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
const breadcrumb: Array<Breadcrumb> = [{
label: dataset.title,
href: `/datasets/${dataset.id}`
}]
const onAcceptResult = async () => {
await api(APIEndPoints.ApproveResult, { requestId: request.id });
setIsReviewResultModalOpen(false);
window.location.reload();
}
const onAcceptRequest = async () => {
await api(APIEndPoints.AcceptRequest, { requestId: request.id })
// setRequest(data);
window.location.reload();
}
const onNotebookDownload = async () => {
try {
await api(APIEndPoints.DownloadNotebook, { requestId: request.id });
await api(APIEndPoints.DownloadComputationNotebook, { requestId: request.id });
}
catch (error) {
const data = await api(APIEndPoints.UploadNotebook, {
@@ -82,29 +124,60 @@ const RequestDetail = (props: RequestDetailProps) => {
code: request.code
})
if (data) {
await api(APIEndPoints.DownloadNotebook, { requestId: request.id });
await api(APIEndPoints.DownloadComputationNotebook, { requestId: request.id });
}
}
}
const onSubmitResult = async ({
result,
proofFile,
precalWitnessFile,
settingsFile
}: onSubmitResultArgs) => {
if ( proofFile && precalWitnessFile && settingsFile ) {
await api(APIEndPoints.UploadProofAndAssets, {
requestId: request.id,
proofFile,
precalWitnessFile,
settingsFile
}).then((data) => {
api(APIEndPoints.SubmitRequestResult, {
requestId: request.id,
result,
})
})
} else {
// TODO handle errors
}
setSubmitResultModalOpen(false);
window.location.reload();
}
return (
<div>
<NavBar />
<Hero header={ title } subheader={ description }/>
<Layout>
<div className="text-2xl font-bold">
Request: {request.title}
<div>
<div
className={`text-sm ${statusColor} rounded px-2 text-white inline-block`}
>
{statusText}
</div>
<SmallHero header={
<div className="flex gap-4 items-center">
{ request.title }
<div
className={`text-sm ${statusColor} rounded px-2 text-white inline-block`}
>
{statusText}
</div>
</div>
} subheader={ request.description } breadcrumb={breadcrumb}/>
{isReviewResultModalOpen &&
<ReviewResultModal onClose={() => setIsReviewResultModalOpen(false)} onAccept={onAcceptResult} result={request.result}/>
}
{submitResultModalOpen &&
<SubmitResultModal onClose={() => setSubmitResultModalOpen(false)} onSubmit={onSubmitResult}/>
}
<Layout>
<div className="my-4">
{isDatasetOwner
@@ -112,33 +185,76 @@ const RequestDetail = (props: RequestDetailProps) => {
: <ConsumerActionBar request={request} />
}
</div>
<div className="flex flex-row w-full gap-4">
<div className="flex-grow">
<div className="bg-gray-200 rounded my-4 p-4">
<div className="text-lg font-bold">
Request Code
<div className="bg-gray-100 rounded text-sm font-light p-2 font-mono whitespace-pre-line">
{request.code}
</div>
</div>
</div>
<div className="flex flex-col bg-gray-200 rounded my-4 p-4">
<div className="text-lg font-bold">
{isDatasetOwner ? 'Submitted Result' : 'Result'}
<div className="flex flex-col w-full gap-4">
<div className="flex flex-row w-full gap-4">
{isCompleted &&
<div className="flex flex-col bg-gray-50 border-1 border-gray-200 rounded my-4 p-4 text-indigo-800 w-2/3">
<div className="flex items-center font-bold text-lg">
{isDatasetOwner ? 'Submitted Result' : 'Result'} {request.resultApproved ? <span className="text-xs text-green-900">&nbsp;<FontAwesomeIcon icon={faCheckCircle} /> (Verified)</span> : ""}
</div>
<div className="bg-gray-100 rounded text-sm font-light p-2">
{request.result}
</div>
<div className="my-4">
<div className="flex mt-4 gap-2">
{!isDatasetOwner &&
<Button
variant={ButtonVariant.QUARTERY}
onClick={() => setIsReviewResultModalOpen(true)}
>
Review Result
</Button>
}
<Button
disabled={!request.isCompleted}
variant={request.isCompleted ? ButtonVariant.PRIMARY : ButtonVariant.DISABLED}
>Verify Computation</Button>
</div>
variant={request.isCompleted ? ButtonVariant.QUARTERY : ButtonVariant.DISABLED}
onClick={onVerifierNotebookDownload}
>Download Verification Notebook</Button>
</div>
</div>
}
{isCompleted &&
<div className="w-1/3">
<CryptographicAssets datasetId={dataset.id} requestId={request.id} resultReady={request.isCompleted}/>
</div>
}
</div>
<div className="bg-gray-50 border-1 border-gray-200 rounded my-4 p-4">
<div className="text-lg font-bold text-indigo-800 my-2">
Request Code
</div>
<div className="bg-gray-100 rounded text-sm font-light p-6 font-mono whitespace-pre-line text-gray-800 border-1 border-gray-200">
{request.code}
</div>
<div className="flex gap-2 mt-4 mb-2">
{(isDatasetOwner && !isAccepted) &&
<div>
<Button
variant={ButtonVariant.QUARTERY}
onClick={onAcceptRequest}
>
Accept Computation Request
</Button>
</div>
}
{(isDatasetOwner && isAccepted && !isCompleted) &&
<div>
<Button
variant={ButtonVariant.QUARTERY}
onClick={() => setSubmitResultModalOpen(true)}
>
Submit Result
</Button>
</div>
}
<div>
<Button
onClick={onNotebookDownload}
variant={ButtonVariant.QUARTERY}
>Download Jupyter Notebook</Button>
</div>
</div>
<div className="w-1/4">
<CryptographicAssets requestId={request.id} resultReady={request.isCompleted}/>
</div>
</div>
</Layout>
@@ -152,21 +268,16 @@ type ActionBarProps = {
}
type onSubmitResultArgs = {
result: string;
proofFile: File | null, // TODO remove null
vkFile: File | null, // TODO remove null
modelOnnxFile: File | null, // TODO remove null
srsFile: File | null, // TODO remove null
precalWitnessFile: File | null, // TODO remove null
settingsFile: File | null, // TODO remove null
}
const extractResult = (proofFile: File) => {
return;
}
const OwnerActionBar = (props: ActionBarProps) => {
const { datasetId, request: initialRequest } = props;
const [ request, setRequest ] = useState<FullRequest>(initialRequest);
const { isAccepted } = request;
const { isAccepted, isCompleted, resultApproved } = request;
const [ submitResultModalOpen, setSubmitResultModalOpen ] = useState<boolean>(false);
const onAcceptRequest = async () => {
@@ -185,90 +296,71 @@ const OwnerActionBar = (props: ActionBarProps) => {
// TODO don't upload srsFile, make them select from a list
const onSubmitResult = async ({
result,
proofFile,
vkFile,
modelOnnxFile,
srsFile,
precalWitnessFile,
settingsFile
}: onSubmitResultArgs) => {
// TODO extract results instead (and maybe upload?? or don't??)
// await api(APIEndPoints.SubmitRequestResult, { requestId: request.id, result })
if ( proofFile && vkFile && modelOnnxFile && srsFile && settingsFile ) {
if ( proofFile && precalWitnessFile && settingsFile ) {
await api(APIEndPoints.UploadProofAndAssets, {
datasetId,
requestId: request.id,
proofFile,
vkFile,
modelOnnxFile,
srsFile,
precalWitnessFile,
settingsFile
}).then((data) => {
api(APIEndPoints.SubmitRequestResult, {
requestId: request.id,
result,
})
})
} else {
// TODO handle errors
}
setSubmitResultModalOpen(false);
window.location.reload();
}
const onNotebookDownload = async () => {
try {
await api(APIEndPoints.DownloadNotebook, { requestId: request.id });
}
catch (error) {
const data = await api(APIEndPoints.UploadNotebook, {
requestId: request.id,
code: request.code
})
if (data) {
await api(APIEndPoints.DownloadNotebook, { requestId: request.id });
}
}
}
return (
<div>
{submitResultModalOpen &&
<SubmitResultModal onClose={() => setSubmitResultModalOpen(false)} onSubmit={onSubmitResult}/>
}
<div
className="flex gap-4"
>
{!isAccepted &&
<div>
<Button
onClick={onAcceptRequest}
>
Accept Computation Request
</Button>
</div>
}
{isAccepted &&
<div>
<Button
onClick={() => setSubmitResultModalOpen(true)}
>Submit Result</Button>
</div>
}
<div>
<Button
onClick={onNotebookDownload}
variant={ButtonVariant.SECONDARY}
>Download Jupyter Notebook</Button>
</div>
{!isAccepted &&
<div className="flex w-full rounded-lg bg-gray-500 py-8 px-6 text-stone-50">
This computation request is awaiting your review.
</div>
}
{(isAccepted && !isCompleted) &&
<div className="flex w-full rounded-lg bg-yellow-400 py-8 px-6 text-stone-50 font-bold">
You have accepted this computation request. Please download the Jupyter Notebook and follow the instructions to run the computation.
</div>
}
{(isCompleted && !resultApproved) &&
<div className="flex w-full rounded-lg bg-green-400 py-8 px-6 text-stone-50 font-bold">
You submitted the result for this computation and are currently awaiting verification.
</div>
}
{(resultApproved) &&
<div className="flex w-full rounded-lg bg-green-800 py-8 px-6 text-stone-50 font-bold">
This request has been completed.
</div>
}
</div>
)
}
const ConsumerActionBar = (props: ActionBarProps) => {
const { request } = props;
const { isCompleted } = request;
const { isCompleted, isAccepted, resultApproved } = request;
const [ isReviewResultModalOpen, setIsReviewResultModalOpen ] = useState<boolean>(false);
const onNotebookDownload = async () => {
try {
await api(APIEndPoints.DownloadNotebook, { requestId: request.id });
await api(APIEndPoints.DownloadComputationNotebook, { requestId: request.id });
}
catch (error) {
const data = await api(APIEndPoints.UploadNotebook, {
@@ -276,27 +368,66 @@ const ConsumerActionBar = (props: ActionBarProps) => {
code: request.code
})
if (data) {
await api(APIEndPoints.DownloadNotebook, { requestId: request.id });
await api(APIEndPoints.DownloadComputationNotebook, { requestId: request.id });
}
}
}
const onAcceptResult = () => {
const onAcceptResult = async () => {
await api(APIEndPoints.ApproveResult, { requestId: request.id });
setIsReviewResultModalOpen(false);
window.location.reload();
}
const onClickVerifyComputation = () => {
}
const onVerifierNotebookDownload = async () => {
const notebook = await generateVerifierNotebook(request.code);
const notebookBlob = new Blob([notebook], { type: 'application/x-ipynb+json' });
const url = URL.createObjectURL(notebookBlob);
console.log('verifier notebook gen')
const link = document.createElement('a');
link.href = url;
link.download = 'verifier.ipynb'; // Set the desired file name
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
return (
<div
className="flex gap-4"
>
{!isAccepted &&
<div className="flex w-full rounded-lg bg-gray-500 py-8 px-6 text-stone-50">
The owner of this dataset has not reviewed this request yet.
</div>
}
{(isAccepted && !isCompleted) &&
<div className="flex w-full rounded-lg bg-yellow-400 py-8 px-6 text-gray-900 font-bold">
This request has been accepted by the dataset owner. The result and proof will be uploaded here later.
</div>
}
{(isCompleted && !resultApproved) &&
<div className="flex w-full rounded-lg bg-green-400 py-8 px-6 text-gray-50 font-bold items-center gap-2">
<FontAwesomeIcon icon={faCircleCheck} /> This dataset owner has submitted the result and is awaiting a review.
</div>
}
{resultApproved &&
<div className="flex w-full rounded-lg bg-green-800 py-8 px-6 text-gray-50 font-bold items-center gap-2">
<FontAwesomeIcon icon={faCircleCheck} /> This request has been completed.
</div>
}
{isReviewResultModalOpen &&
<ReviewResultModal onClose={() => setIsReviewResultModalOpen(false)} onAccept={onAcceptResult} result={request.result}/>
}
{isCompleted &&
{/* {isCompleted &&
<>
<div>
<Button
@@ -305,41 +436,101 @@ const ConsumerActionBar = (props: ActionBarProps) => {
</div>
<div>
<Button
onClick={onClickVerifyComputation}
onClick={onVerifierNotebookDownload}
>
Verify Computation
Download Verification Notebook
</Button>
</div>
</>
}
<div>
<Button
onClick={onNotebookDownload}
variant={ButtonVariant.SECONDARY}
>Download Jupyter Notebook</Button>
</div>
} */}
</div>
)
}
type CryptographicAssetsProps = {
datasetId: string;
requestId: string;
resultReady?: boolean;
}
const CryptographicAssets = ({ requestId, resultReady = true } : CryptographicAssetsProps) => {
const CryptographicAssets = ({ datasetId, requestId, resultReady = true } : CryptographicAssetsProps) => {
const [ doesDataCommitmentExist, setDoesDataCommitmentExist ] = useState<boolean | null>(null);
const [ doesProofExist, setDoesProofExist ] = useState<boolean | null>(null);
const [ doesPrecalWitnessExist, setDoesPrecalWitnessExist ] = useState<boolean | null>(null);
const [ doSettingsExist, setDoSettingsExist ] = useState<boolean | null>(null);
useEffect(() => {
api(APIEndPoints.CheckFileExists, {
bucketName: 'proof_assets',
filePath: `${datasetId}`,
fileName: 'data_commitment.json'
}).then(result => setDoesDataCommitmentExist(result))
api(APIEndPoints.CheckFileExists, {
bucketName: 'proof_assets',
filePath: `${datasetId}/${requestId}`,
fileName: 'model.pf'
}).then(result => setDoesProofExist(result))
api(APIEndPoints.CheckFileExists, {
bucketName: 'proof_assets',
filePath: `${datasetId}/${requestId}`,
fileName: 'precal_witness.json'
}).then(result => setDoesPrecalWitnessExist(result))
api(APIEndPoints.CheckFileExists, {
bucketName: 'proof_assets',
filePath: `${datasetId}/${requestId}`,
fileName: 'settings.json'
}).then(result => setDoSettingsExist(result))
})
return (
<div className="bg-gray-200 rounded my-4 p-4">
<div className="text-lg font-bold">
<div className="bg-gray-50 border-1 border-gray-200 rounded my-4 p-4">
<div className="text-lg font-bold text-indigo-800 my-2">
Cryptographic Assets
</div>
{resultReady ? (
<div className="flex flex-col">
<Link href=""><FontAwesomeIcon icon={faFileAlt} /> Proof</Link>
<Link href=""><FontAwesomeIcon icon={faFileAlt} /> Verification Key</Link>
<Link href=""><FontAwesomeIcon icon={faFileAlt} /> Model Onnx</Link>
<Link href=""><FontAwesomeIcon icon={faFileAlt} /> Srs</Link>
<Link href=""><FontAwesomeIcon icon={faFileAlt} /> Settings</Link>
{doesDataCommitmentExist
? <div
className="hover:cursor-pointer"
onClick={() => api(APIEndPoints.DownloadDataCommitment, { datasetId })}><FontAwesomeIcon icon={faFileAlt}
/> Data Commitment</div>
: <div>
{doesDataCommitmentExist === null
? <span><FontAwesomeIcon icon={faFileAlt} /> Checking...</span>
: <span className="text-red-400"><FontAwesomeIcon icon={faWarning} /> Missing data commitment</span>}
</div>
}
{doesProofExist
? <div
className="hover:cursor-pointer"
onClick={() => api(APIEndPoints.DownloadProofAndAssets, {datasetId, requestId, file: 'proof'})}
><FontAwesomeIcon icon={faFileAlt} /> Proof</div>
: <div>
{doesProofExist === null
? <span><FontAwesomeIcon icon={faFileAlt} /> Checking...</span>
: <span className="text-red-400"><FontAwesomeIcon icon={faWarning} /> Missing proof</span>}
</div>
}
{doesPrecalWitnessExist
? <div
className="hover:cursor-pointer"
onClick={() => api(APIEndPoints.DownloadProofAndAssets, {datasetId, requestId, file: 'precal'})}
><FontAwesomeIcon icon={faFileAlt} /> Precal witness</div>
: <div>
{doesPrecalWitnessExist === null
? <span><FontAwesomeIcon icon={faFileAlt} /> Checking...</span>
: <span className="text-red-400"><FontAwesomeIcon icon={faWarning} /> Missing precal witness</span>}
</div>
}
{doSettingsExist
? <div
className="hover:cursor-pointer"
onClick={() => api(APIEndPoints.DownloadProofAndAssets, {datasetId, requestId, file: 'settings'})}
><FontAwesomeIcon icon={faFileAlt} /> Settings</div>
: <div>
{doSettingsExist === null
? <span><FontAwesomeIcon icon={faFileAlt} /> Checking...</span>
: <span className="text-red-400"><FontAwesomeIcon icon={faWarning} /> Missing settings</span>}
</div>
}
</div>
)
: (

View File

@@ -2,19 +2,20 @@ import { FormInput } from "@/components/common/Form";
import Hero from "@/components/common/Hero";
import Layout from "@/components/common/Layout";
import { DatasetCard } from "@/components/datasets/DatasetCard";
import { DatasetPreview } from "@/types/dataset";
import { Dataset, DatasetPreview, DatasetResponse, convertDatasetResponseToDataset } from "@/types/dataset";
import { APIEndPoints, api } from "@/utils/api";
import { useUser } from "@/utils/session";
import { useEffect, useState } from "react";
const Datasets = () => {
const [ datasets, setDatasets ] = useState<Array<DatasetPreview>>([]);
const [ filteredDatasets, setFilteredDatasets ] = useState<Array<DatasetPreview>>([]);
const [ datasets, setDatasets ] = useState<Array<Dataset>>([]);
const [ filteredDatasets, setFilteredDatasets ] = useState<Array<Dataset>>([]);
useEffect(() => {
api(APIEndPoints.Datasets).then((response) => {
setDatasets(response);
setFilteredDatasets(response);
const convertedDatasets = response.map((dataset: DatasetResponse) => convertDatasetResponseToDataset(dataset))
setDatasets(convertedDatasets);
setFilteredDatasets(convertedDatasets);
})
}, [setDatasets])
@@ -22,8 +23,9 @@ const Datasets = () => {
const user = useUser();
const DatasetSearchBar = (
<div className="mx-auto w-2/4">
<div className="w-3/4">
<FormInput
placeholder="Dataset name, column name, etc."
onChange={(e) => {
setFilteredDatasets(
datasets.filter(dataset =>
@@ -37,15 +39,15 @@ const Datasets = () => {
return (
<div>
<Hero
header='Datasets'
subheader="Search for a dataset to explore"
header='Browse datasets'
subheader="Explore datasets from our community. Get your questions about the data answered."
action={DatasetSearchBar}
/>
{datasets && (
<Layout>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{filteredDatasets.map((dataset) => (
<DatasetCard previewImage='' dataset={dataset} key={dataset.id} />
<DatasetCard dataset={dataset} key={dataset.id} />
))
}
</div>

View File

@@ -4,7 +4,7 @@ import FormWrapper, { FormFile, FormInput, FormItem, FormRadioSelect, FormTextAr
import Hero from "@/components/common/Hero";
import Layout from "@/components/common/Layout";
import NavBar from "@/components/common/NavBar";
import { Dataset } from "@/types/dataset";
import { Dataset, Schema } from "@/types/dataset";
import { APIEndPoints, api } from "@/utils/api";
import { generateDataCommitment, initialize } from "@/utils/ezkl";
import { useUser } from "@/utils/session";
@@ -35,13 +35,20 @@ enum GenerateDataCommitmentOptions {
BROWSER = 'browser',
}
enum SchemaOptions {
TEXT = 'text',
JSON_FILE = 'json_file'
}
const UploadDataset = () => {
const [ selectedDataCommitmentOption, setSelectedDataCommitmentOption ] = useState<GenerateDataCommitmentOptions>(GenerateDataCommitmentOptions.NOTEBOOK)
const [ schemaOption, setSchemaOption ] = useState<SchemaOptions>(SchemaOptions.TEXT);
const [ dataFile, setDataFile ] = useState<File | null>();
const [ dataCommitmentFile, setDataCommitmentFile ] = useState<File | null>();
const [ redirecting, setRedirecting ] = useState<boolean>(false);
const [ datasetId, setDatasetId ] = useState<string | null>();
const [ schema, setSchema ] = useState<Schema | null>(null);
const { register, handleSubmit, formState } = useForm<Dataset>();
const { errors } = formState;
@@ -61,6 +68,9 @@ const UploadDataset = () => {
title: data.title,
description: data.description,
ownerId: user?.id,
schema,
rows: data.rows,
columns: data.columns,
})
if (dataCommitmentFile) {
await api(APIEndPoints.UploadDataCommitment, {
@@ -110,6 +120,30 @@ const UploadDataset = () => {
{...register('description', { required: true })}
/>
</FormItem>
<FormItem>
<div
className="mb-2 text-md font-medium text-gray-900"
>
Number of columns in this dataset
</div>
<FormInput
errors={errors}
errorMessage={'Just a test error'}
{...register('rows', { required: false })}
/>
</FormItem>
<FormItem>
<div
className="mb-2 text-md font-medium text-gray-900"
>
Number of rows in this dataset (excluding the headers)
</div>
<FormInput
errors={errors}
errorMessage={'Just a test error'}
{...register('columns', { required: false })}
/>
</FormItem>
</Card>
<Card>
<FormItem>
@@ -118,11 +152,45 @@ const UploadDataset = () => {
>
Data Schema
</div>
<FormTextArea
errors={errors}
errorMessage="text area error"
{...register('schema', { required: true })}
/>
<FormRadioSelect
options={[
{
value: SchemaOptions.TEXT,
label: 'Paste the JSON schema',
},
{
value: SchemaOptions.JSON_FILE,
label: 'Upload JSON schema file',
},
]}
onChange={(o) => setSchemaOption(o.value as SchemaOptions)} // TODO fix
/>
{schemaOption === SchemaOptions.TEXT &&
<FormTextArea
errors={errors}
errorMessage="text area error"
{...register('schema', { required: true })}
/>
}
{schemaOption === SchemaOptions.JSON_FILE &&
<FormFile onChange={(e) => {
// TODO also option to copy paste
if (e.target.files) {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (event) => {
try {
//@ts-ignore
const json = JSON.parse(event.target.result);
setSchema(json);
} catch (error) {
console.error("Error parsing JSON:", error);
}
};
reader.readAsText(file);
}
}} />
}
</FormItem>
</Card>
<Card>

View File

@@ -3,6 +3,10 @@
@import 'tailwindcss/utilities';
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
body {
background-color: rgb(238 242 255);
}
/* Custom styles for the hover underline effect */
.menu-item::after {
content: '';

File diff suppressed because one or more lines are too long

View File

@@ -1,185 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 39,
"metadata": {},
"outputs": [],
"source": [
"%pip install zkstats\n",
"\n",
"import os\n",
"from zkstats.core import (\n",
" prover_gen_settings,\n",
" prover_gen_proof,\n",
" verifier_setup,\n",
" verifier_verify,\n",
")\n",
"from zkstats.computation import computation_to_model\n",
"\n",
"cwd = os.getcwd()\n",
"\n",
"# FIXME: fill this in with the path to your data\n",
"data_path = f\"{cwd}/data.json\"\n",
"\n",
"# Paths to the output files\n",
"output_dir = f\"{cwd}/out\"\n",
"os.makedirs(output_dir, exist_ok=True)\n",
"model_onnx_path = f\"{output_dir}/model.onnx\"\n",
"compiled_model_path = f\"{output_dir}/model.compiled\"\n",
"\n",
"pk_path = f\"{output_dir}/model.pk\"\n",
"vk_path = f\"{output_dir}/model.vk\"\n",
"proof_path = f\"{output_dir}/model.pf\"\n",
"settings_path = f\"{output_dir}/settings.json\"\n",
"witness_path = f\"{output_dir}/witness.json\"\n",
"comb_data_path = f\"{output_dir}/comb_data.json\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## User-defined Computation\n",
"\n",
"A computation should be of type `TComputation`. For example, the following code snippet defines a computation that computes the sum of the private data.\n",
"\n",
"```python\n",
"def computation(state: State, x: list[torch.Tensor]):\n",
" out_0 = state.median(x[0])\n",
" out_1 = state.median(x[1])\n",
" return state.mean(torch.tensor([out_0, out_1]).reshape(1,-1,1))\n",
"```\n",
"\n",
"FIXME: The following code snippet is entirely from the user. You MUST check\n",
"1. the code only performs zkstats-related operations.\n",
"2. the computation must not leak any information about the private data."
]
},
{
"cell_type": "code",
"execution_count": 40,
"metadata": {},
"outputs": [],
"source": [
"# This is just a dummy computation. Replace it with user's computation\n",
"#\n",
"# import torch\n",
"# from zkstats.computation import State\n",
"#\n",
"# def computation(state: State, x: list[torch.Tensor]):\n",
"# x_0 = x[0]\n",
"# out_0 = state.median(x_0)\n",
"# out_1 = state.median(x_0)\n",
"# return state.mean(torch.tensor([out_0, out_1]).reshape(1,-1,1))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Generate settings and setup with user's computation."
]
},
{
"cell_type": "code",
"execution_count": 41,
"metadata": {},
"outputs": [],
"source": [
"_, model = computation_to_model(computation)\n",
"prover_gen_settings(\n",
" [data_path],\n",
" comb_data_path,\n",
" model,\n",
" model_onnx_path,\n",
" \"default\",\n",
" \"resources\",\n",
" settings_path,\n",
")\n",
"verifier_setup(model_onnx_path, compiled_model_path, settings_path, vk_path, pk_path)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Generate proof with your data and user's computation."
]
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {},
"outputs": [],
"source": [
"prover_gen_proof(\n",
" model_onnx_path,\n",
" comb_data_path,\n",
" witness_path,\n",
" compiled_model_path,\n",
" settings_path,\n",
" proof_path,\n",
" pk_path,\n",
")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Verify the proof to ensure it is correct"
]
},
{
"cell_type": "code",
"execution_count": 43,
"metadata": {},
"outputs": [],
"source": [
"verifier_verify(proof_path, settings_path, vk_path)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Print the file paths. You should share the following files back to the user for them to verify the proof. You **SHOULD NOT** share more files otherwise data might be leaked."
]
},
{
"cell_type": "code",
"execution_count": 44,
"metadata": {},
"outputs": [],
"source": [
"model_onnx_path, settings_path, proof_path, vk_path\n",
"print(\"Model onnx:\\t\\t\", model_onnx_path)\n",
"print(\"Settings:\\t\\t\", settings_path)\n",
"print(\"Proof:\\t\\t\\t\", proof_path)\n",
"print(\"Verification key:\\t\", vk_path)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.6"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -0,0 +1,255 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"import os\n",
"from zkstats.core import (\n",
" setup,\n",
")\n",
"from zkstats.computation import computation_to_model\n",
"\n",
"cwd = os.getcwd()\n",
"\n",
"\n",
"# Paths to the output files\n",
"output_dir = f\"{cwd}/out\"\n",
"os.makedirs(output_dir, exist_ok=True)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"# Shared by the data provider beforehand\n",
"data_shape = {'x': 7, 'y': 7}\n",
"data_commitment_path = f\"{output_dir}/data_commitment.json\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"User select the columns"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"# FIXME: this should be provided by users\n",
"# selected_columns = [\"x\", \"y\"]\n",
"selected_columns = [\"x\"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## User-defined Computation\n",
"\n",
"A computation should be of type `TComputation`. For example, the following code snippet defines a computation that computes the sum of the private data.\n",
"\n",
"```python\n",
"def computation(state: State, x: list[torch.Tensor]):\n",
" out_0 = state.median(x[0])\n",
" out_1 = state.median(x[1])\n",
" return state.mean(torch.cat([out_0.unsqueeze(0), out_1.unsqueeze(0)]).reshape(-1,1))\n",
"```\n",
"\n",
"FIXME: The following code snippet is entirely from the user. You MUST check\n",
"1. the code only performs zkstats-related operations.\n",
"2. the computation must not leak any information about the private data."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"# This is just a dummy computation. Replace it with user's computation\n",
"import torch\n",
"from zkstats.computation import State\n",
"\n",
"def computation(state: State, x: list[torch.Tensor]):\n",
" # out_0 = state.median(x[0])\n",
" # out_1 = state.median(x[1])\n",
" # # return state.mean(torch.cat([out_0.unsqueeze(0), out_1.unsqueeze(0)]).reshape(-1,1)), out_0\n",
" # return out_0, out_1\n",
" return state.mean(x[0]), state.median(x[0])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Get proof, settings, and precal_witness from the data provider"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"proof_path = f\"{output_dir}/model.pf\"\n",
"settings_path = f\"{output_dir}/settings.json\"\n",
"precal_witness_path = f\"{output_dir}/precal_witness.json\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Verify the proof to ensure it is correct\n",
"NOTE: The following section is to illustrate what should be done on the user (data consumer) side. This step is not required by the data provider."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/mhchia/Library/Caches/pypoetry/virtualenvs/zkstats-brXmXluj-py3.12/lib/python3.12/site-packages/torch/onnx/symbolic_opset9.py:2174: FutureWarning: 'torch.onnx.symbolic_opset9._cast_Bool' is deprecated in version 2.0 and will be removed in the future. Please Avoid using this function and create a Cast node instead.\n",
" return fn(g, to_cast_func(g, input, False), to_cast_func(g, other, False))\n",
"/Users/mhchia/Library/Caches/pypoetry/virtualenvs/zkstats-brXmXluj-py3.12/lib/python3.12/site-packages/torch/onnx/utils.py:1703: UserWarning: The exported ONNX model failed ONNX shape inference. The model will not be executable by the ONNX Runtime. If this is unintended and you believe there is a bug, please report an issue at https://github.com/pytorch/pytorch/issues. Error reported by strict ONNX shape inference: [ShapeInferenceError] (op_type:Where, node name: /Where_1): Y has inconsistent type tensor(float) (Triggered internally at /Users/runner/work/pytorch/pytorch/pytorch/torch/csrc/jit/serialization/export.cpp:1490.)\n",
" _C._check_onnx_proto(proto)\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"==== setting up ezkl ====\n",
"Time setup: 0.5691819190979004 seconds\n"
]
},
{
"data": {
"text/plain": [
"[51.5, 46.25]"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import json\n",
"import numpy as np\n",
"from typing import Dict\n",
"\n",
"\n",
"def create_dummy(shape_info: Dict[str, int], dummy_data_path: str) -> None:\n",
" \"\"\"\n",
" Create a dummy data file with randomized data based on the provided shape information.\n",
"\n",
" Parameters:\n",
" - shape_info (dict): A dictionary where keys are column names and values are the number of elements (shape).\n",
" - dummy_data_path (str): The path to save the dummy data file.\n",
" \"\"\"\n",
" dummy_data = {}\n",
" for col, length in shape_info.items():\n",
" # Generate random data for each column\n",
" dummy_data[col] = np.round(np.random.uniform(0, 100, length), 1).tolist()\n",
"\n",
" with open(dummy_data_path, 'w') as f:\n",
" json.dump(dummy_data, f)\n",
"\n",
"\n",
"from zkstats.core import verifier_define_calculation, verifier_verify\n",
"\n",
"verifier_model_path = f\"{output_dir}/verifier_model.onnx\"\n",
"verifier_compiled_model_path = f\"{output_dir}/verifier_model.compiled\"\n",
"verifier_vk_path = f\"{output_dir}/verifier_model.vk\"\n",
"verifier_pk_path = f\"{output_dir}/verifier_model.pk\"\n",
"dummy_data_path = f\"{output_dir}/dummy_data.json\"\n",
"sel_dummy_data_path = f\"{output_dir}/sel_dummy_data.json\"\n",
"\n",
"# NOTE: generate the verifier model with the `precal_witness_path` provided by the prover\n",
"_, verifier_model = computation_to_model(computation, precal_witness_path, isProver=False)\n",
"# Determine which srs to use with the logrows in the settings.json\n",
"with open(settings_path, \"r\") as f:\n",
" settings = json.load(f)\n",
"logrows = int(settings[\"run_args\"][\"logrows\"])\n",
"srs_path = f'~/.ezkl/srs/kzg{logrows}.srs'\n",
"\n",
"# create dummy data with the same shape as the original data\n",
"create_dummy(data_shape, dummy_data_path)\n",
"# generate the verifier model given the dummy data and the selected columns\n",
"verifier_define_calculation(dummy_data_path, selected_columns, sel_dummy_data_path, verifier_model, verifier_model_path)\n",
"# generate the verification key\n",
"setup(verifier_model_path, verifier_compiled_model_path, settings_path, verifier_vk_path, verifier_pk_path)\n",
"# verify the proof\n",
"verifier_verify(proof_path, settings_path, verifier_vk_path, selected_columns, data_commitment_path)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Print the file paths. You should share the following files back to the user for them to verify the proof. You **SHOULD NOT** share more files otherwise data might be leaked."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Model onnx:\t\t /Users/mhchia/projects/work/pse/demo-next/public/assets/out/verifier_model.onnx\n",
"Settings:\t\t /Users/mhchia/projects/work/pse/demo-next/public/assets/out/settings.json\n",
"Proof:\t\t\t /Users/mhchia/projects/work/pse/demo-next/public/assets/out/model.pf\n",
"Verification key:\t /Users/mhchia/projects/work/pse/demo-next/public/assets/out/verifier_model.vk\n",
"Srs path:\t\t ~/.ezkl/srs/kzg11.srs\n"
]
}
],
"source": [
"print(\"Model onnx:\\t\\t\", verifier_model_path)\n",
"print(\"Settings:\\t\\t\", settings_path)\n",
"print(\"Proof:\\t\\t\\t\", proof_path)\n",
"print(\"Verification key:\\t\", verifier_vk_path)\n",
"print(\"Srs path:\\t\\t\", srs_path)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.1"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -13,6 +13,12 @@ const config: Config = {
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
colors: {
},
borderWidth: {
"1": "1px",
}
},
},
plugins: [],

View File

@@ -11,7 +11,7 @@ export type DatasetPreview = {
}
export type Dataset = DatasetPreview & {
schema?: []; // TODO
schema?: Record<string, any>;
testDataUrl?: string;
sourceDescription?: string;
acknowledgement?: string;
@@ -19,6 +19,8 @@ export type Dataset = DatasetPreview & {
glossary?: []; // TODO
ownerId?: string;
//discussionId?: number; // No discussion id here, just make it a foreign key on the discussion table
rows?: number;
columns?: number;
}
export type UserDatasetRequests = Array<UserDatasetRequest>;
@@ -35,6 +37,9 @@ export type DatasetResponse = {
description: string;
updated_at: string;
owner_id: string;
schema?: Record<string, any>;
rows?: number;
columns?: number;
}
export const convertDatasetResponseToDataset = (datasetResponse: DatasetResponse): Dataset => {
@@ -45,5 +50,22 @@ export const convertDatasetResponseToDataset = (datasetResponse: DatasetResponse
description: datasetResponse.description,
updatedAt: datasetResponse.updated_at,
ownerId: datasetResponse.owner_id,
schema: datasetResponse.schema,
rows: datasetResponse.rows,
columns: datasetResponse.columns,
}
}
export type Schema = {
$schema: string;
title: string;
type: string;
properties: {
[key: string]: {
type: string;
description?: string;
format?: string;
};
};
required: string[];
}

View File

@@ -53,3 +53,8 @@ export const convertRawRequestToFullRequest = (rawRequest: RawRequest): FullRequ
resultApproved: rawRequest.result_approved,
}
}
export type Breadcrumb = {
label: string;
href: string;
}

View File

@@ -3,7 +3,8 @@
import useMockData from "@/mocks";
import { NewRequest } from "@/types/request";
import { createClient } from '@supabase/supabase-js'
import { generateJupyterNotebook } from "./notebook";
import { generateProverNotebook } from "./notebook";
import { Schema } from "@/types/dataset";
const useMock = process.env.NEXT_PUBLIC_USE_MOCK === 'true';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
@@ -21,11 +22,17 @@ export enum APIEndPoints {
UploadDataCommitment = 'upload_data_commitment',
Requests = 'requests',
DownloadNotebook = 'download_notebook',
DownloadComputationNotebook = 'download_computation_notebook',
DownloadVerificationNotebook = 'download_verification_notebook',
UploadNotebook = 'upload_noteobook',
AcceptRequest = 'accept_request',
SubmitRequestResult = 'submit_request_result',
UploadProofAndAssets = 'upload_proof_and_assets',
DownloadProofAndAssets = 'download_proof_and_assets',
ApproveResult = 'accept_result',
CheckFileExists = 'check_file_exists',
DownloadDataCommitment = 'download_data_commitment',
SignUp = 'signup',
SignInWithPassword = 'siginin_with_password',
@@ -41,6 +48,9 @@ type AddDatasetOptions = {
title: string;
description: string;
ownerId: string;
schema: Schema | null;
rows?: number;
columns?: number;
}
type UploadDataCommitmentOptions = {
@@ -77,7 +87,11 @@ type GetRequestsOptions = {
datasetId: string;
}
type DownloadNotebookOptions = {
type DownloadComputationNotebookOptions = {
requestId: string;
}
type DownloadVerificationNotebookOptions = {
requestId: string;
}
@@ -99,12 +113,30 @@ type UploadProofAndAssetsOptions = {
datasetId: string;
requestId: string;
proofFile: File,
vkFile: File,
modelOnnxFile: File,
srsFile: File,
precalWitnessFile: File,
settingsFile: File,
}
type DownloadProofAndAssetsOptions = {
datasetId: string;
requestId: string;
file: 'all' | 'proof' | 'precal' | 'settings'
}
type ApproveResultOptions = {
requestId: string; // TODO make it so that only request sender can approve
}
type CheckFileExistsOptions = {
bucketName: string;
filePath: string;
fileName: string;
}
type DownloadDataCommitmentOptions = {
datasetId: string;
}
export type Options = undefined
| GetDatasetOptions
| AddDatasetOptions
@@ -114,9 +146,14 @@ export type Options = undefined
| NewRequestOptions
| GetRequestOptions
| GetRequestsOptions
| DownloadNotebookOptions
| DownloadComputationNotebookOptions
| DownloadVerificationNotebookOptions
| SubmitRequestResultOptions
| UploadProofAndAssetsOptions
| DownloadProofAndAssetsOptions
| ApproveResultOptions
| CheckFileExistsOptions
| DownloadDataCommitmentOptions
export const api = async (
@@ -138,11 +175,17 @@ export const api = async (
case APIEndPoints.AddRequest: return addRequest(options as NewRequestOptions);
case APIEndPoints.Requests: return getRequests(options as GetRequestsOptions);
// Data owner operations
case APIEndPoints.DownloadNotebook: return downloadNotebook(options as DownloadNotebookOptions);
case APIEndPoints.DownloadComputationNotebook: return downloadComputationNotebook(options as DownloadComputationNotebookOptions);
case APIEndPoints.DownloadVerificationNotebook: return downloadVerificationNotebook(options as DownloadVerificationNotebookOptions);
case APIEndPoints.UploadNotebook: return uploadNotebook(options as UploadNotebookOptions);
case APIEndPoints.AcceptRequest: return acceptRequest(options as AcceptRequestOptions);
case APIEndPoints.SubmitRequestResult: return submitRequestResult(options as SubmitRequestResultOptions);
case APIEndPoints.UploadProofAndAssets: return uploadProofAndAssets(options as UploadProofAndAssetsOptions); // TODO options
case APIEndPoints.UploadProofAndAssets: return uploadProofAndAssets(options as UploadProofAndAssetsOptions);
case APIEndPoints.DownloadProofAndAssets: return downloadProofAndAssets(options as DownloadProofAndAssetsOptions);
case APIEndPoints.ApproveResult: return approveResult(options as ApproveResultOptions);
// Cryptographic assets
case APIEndPoints.CheckFileExists: return checkFileExists(options as CheckFileExistsOptions);
case APIEndPoints.DownloadDataCommitment: return downloadDataCommitment(options as DownloadDataCommitmentOptions);
// AUTH
case APIEndPoints.SignUp: return signUp(options as SignUpOptions);
case APIEndPoints.SignInWithPassword: return signInWithPassword(options as SignInWithPasswordOptions);
@@ -179,6 +222,9 @@ const addDataset = async (options: AddDatasetOptions) => {
title: options.title,
description: options.description,
owner_id: options.ownerId,
schema: options.schema,
rows: options.rows,
columns: options.columns
})
if (data) {
return data;
@@ -225,7 +271,7 @@ const addRequest = async (options: NewRequest) => {
}
const uuid = crypto.randomUUID();
const notebook = await generateJupyterNotebook(options.code);
const notebook = await generateProverNotebook(options.code);
const notebookBlob = new Blob([notebook], { type: 'application/x-ipynb+json' })
const { data, error } = await supabase
@@ -276,7 +322,7 @@ const getRequests = async(options: GetRequestsOptions) => {
}
}
const downloadNotebook = async ({ requestId }: DownloadNotebookOptions) => {
const downloadComputationNotebook = async ({ requestId }: DownloadComputationNotebookOptions) => {
if (!supabase) {
return;
// TODO error
@@ -318,13 +364,55 @@ const downloadNotebook = async ({ requestId }: DownloadNotebookOptions) => {
}
const downloadVerificationNotebook = async ({ requestId }: DownloadVerificationNotebookOptions) => {
if (!supabase) {
return;
// TODO error
}
const { data } = supabase.storage
.from('computations')
.getPublicUrl(`${requestId}-verification.ipynb`);
const { publicUrl } = data;
fetch(publicUrl)
.then(response => {
if (response.ok) return response.blob();
throw new Error('Network response was not ok.');
})
.then(blob => {
// Create an object URL for the blob object
const blobUrl = URL.createObjectURL(blob);
// Create a temporary anchor element
const anchor = document.createElement('a');
// Set the href to the object URL
anchor.href = blobUrl;
anchor.download = `${requestId}-verification.ipynb`; // Set the download attribute with the desired file name
// Append the anchor to the body to make it clickable
document.body.appendChild(anchor);
// Trigger the download by clicking the anchor
anchor.click();
// Clean up by revoking the object URL and removing the anchor element
URL.revokeObjectURL(blobUrl);
anchor.remove();
})
.catch(error => console.error('Download failed:', error));
}
const uploadNotebook = async ({ requestId, code } : UploadNotebookOptions) => {
if (!supabase) {
return;
// TODO error
}
const notebook = await generateJupyterNotebook(code);
const notebook = await generateProverNotebook(code);
const notebookBlob = new Blob([notebook], { type: 'application/x-ipynb+json' })
const { data, error } = await supabase.storage
@@ -403,9 +491,7 @@ const uploadProofAndAssets = async ({
datasetId,
requestId,
proofFile,
vkFile,
modelOnnxFile,
srsFile,
precalWitnessFile,
settingsFile
}: UploadProofAndAssetsOptions) => {
if (!supabase) {
@@ -415,9 +501,7 @@ const uploadProofAndAssets = async ({
const promises = [
supabase.storage.from('proof_assets').upload(`${datasetId}/${requestId}/model.pf`, proofFile),
supabase.storage.from('proof_assets').upload(`${datasetId}/${requestId}/model.vk`, vkFile),
supabase.storage.from('proof_assets').upload(`${datasetId}/${requestId}/model.onnx`, modelOnnxFile),
//supabase.storage.from('proof_assets').upload(`${datasetId}/${requestId}/model.pf`, proofFile) // TODO ???
supabase.storage.from('proof_assets').upload(`${datasetId}/${requestId}/precal_witness.json`, precalWitnessFile),
supabase.storage.from('proof_assets').upload(`${datasetId}/${requestId}/settings.json`, settingsFile)
];
// const { data, error } = await supabase.storage
@@ -428,6 +512,159 @@ const uploadProofAndAssets = async ({
// }
}
const downloadProofAndAssets = async ({
datasetId,
requestId,
file
}: DownloadProofAndAssetsOptions) => {
if (!supabase) {
return;
// TODO error
}
const getPath = (file: 'all' | 'proof' | 'precal' | 'settings') => {
switch (file) {
case ('all'): return `${datasetId}/${requestId}`;
case ('proof'): return `${datasetId}/${requestId}/model.pf`;
case ('precal'): return `${datasetId}/${requestId}/precal_witness.json`;
case ('settings'): return `${datasetId}/${requestId}/settings.json`;
default: return `${datasetId}/${requestId}`;
}
}
const { data } = supabase.storage
.from('proof_assets')
.getPublicUrl(getPath(file));
const { publicUrl } = data;
fetch(publicUrl)
.then(response => {
if (response.ok) return response.blob();
throw new Error('Network response was not ok.');
})
.then(blob => {
// Create an object URL for the blob object
const blobUrl = URL.createObjectURL(blob);
// Create a temporary anchor element
const anchor = document.createElement('a');
// Set the href to the object URL
anchor.href = blobUrl;
anchor.download = `${datasetId}-datacommitment.json`; // Set the download attribute with the desired file name
// Append the anchor to the body to make it clickable
document.body.appendChild(anchor);
// Trigger the download by clicking the anchor
anchor.click();
// Clean up by revoking the object URL and removing the anchor element
URL.revokeObjectURL(blobUrl);
anchor.remove();
})
.catch(error => console.error('Download failed:', error));
}
const approveResult = async ({
requestId,
}: ApproveResultOptions) => {
if (!supabase) {
return;
// TODO error
}
const { data, error } = await supabase
.from('computations')
.update({
result_approved: true,
})
.match({
id: requestId
})
if (data) {
return data
}
}
const checkFileExists = async({
bucketName,
filePath,
fileName
}: CheckFileExistsOptions) => {
// DO I NEED BUCKET NAME HERE?
if (!supabase) {
return;
// TODO error
}
try {
const { data, error } = await supabase
.storage
.from(bucketName)
.list(filePath)
console.log('dddd', bucketName, filePath, data)
if (error) {
console.error('Error listing files:', error)
return false
}
return data.some(file => file.name === fileName)
} catch (e) {
return false
}
}
const downloadDataCommitment = async ({
datasetId,
}: DownloadDataCommitmentOptions) => {
if (!supabase) {
return;
// TODO error
}
const { data } = supabase.storage
.from('proof_assets')
.getPublicUrl(`${datasetId}/data_commitment.json`);
const { publicUrl } = data;
fetch(publicUrl)
.then(response => {
if (response.ok) return response.blob();
throw new Error('Network response was not ok.');
})
.then(blob => {
// Create an object URL for the blob object
const blobUrl = URL.createObjectURL(blob);
// Create a temporary anchor element
const anchor = document.createElement('a');
// Set the href to the object URL
anchor.href = blobUrl;
anchor.download = `${datasetId}-datacommitment.json`; // Set the download attribute with the desired file name
// Append the anchor to the body to make it clickable
document.body.appendChild(anchor);
// Trigger the download by clicking the anchor
anchor.click();
// Clean up by revoking the object URL and removing the anchor element
URL.revokeObjectURL(blobUrl);
anchor.remove();
})
.catch(error => console.error('Download failed:', error));
}
const signUp = async ({ username, email, password }: SignUpOptions) => {
if (!supabase) {
return;

View File

@@ -111,3 +111,23 @@ function generateDataCommitmentForColumn(column: number[], scale: number) {
const deserializedOutput = deserialize(output)
return deserializedOutput[0][0] as string
}
export const extractResult = (proofFile: File) => {
if (proofFile) {
const reader = new FileReader()
reader.onload = (e) => {
try {
const json = JSON.parse(e.target?.result as string)
console.log(json['pretty_public_inputs']['rescaled_outputs'].slice(1,))
return(json['pretty_public_inputs']['rescaled_outputs'].slice(1,))
} catch (error) {
console.log('b')
console.error('Error parsing JSON:', error)
return '';
}
}
reader.readAsText(proofFile)
}
console.log('a')
return '';
}

View File

@@ -1,11 +1,24 @@
const REQUEST_TEMPLATE_URL = '/templates/run_request.ipynb';
const CODE_CELL = 2;
const PROVER_TEMPLATE_URL = '/templates/prover.ipynb';
const PROVER_CODE_CELL = 5;
export const generateJupyterNotebook = async (code: string) => {
export const generateProverNotebook = async (code: string) => {
const splitCode = code.split("\n").map((line) => `${line}\n`);
const response = await fetch(REQUEST_TEMPLATE_URL);
const response = await fetch(PROVER_TEMPLATE_URL);
const template = await response.json();
const codeCell = template.cells[CODE_CELL];
const codeCell = template.cells[PROVER_CODE_CELL];
codeCell.source = splitCode;
return JSON.stringify(template);
}
}
const VERIFIER_TEMPLATE_URL = '/templates/verifier.ipynb';
const VERIFIER_CODE_CELL = 5;
export const generateVerifierNotebook = async ( code: string) => {
const splitCode = code.split("\n").map((line) => `${line}\n`);
const response = await fetch(VERIFIER_TEMPLATE_URL);
const template = await response.json();
const codeCell = template.cells[VERIFIER_CODE_CELL];
codeCell.source = splitCode;
return JSON.stringify(template);
}