Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,17 @@ Here's how to get started:
5. You'll also need a Redis instance to store sessions. Again, feel free to set
this up in whatever way works best for you.

6. Finally, create a .env file in the root directory of your sandbox with
SECRET_KEY, DATABASE_URL, and REDIS_URL entries. The secret key can be
6. [Optional] if you want to test submission end to end, you need to run and setup [discord-cluster-manager](https://github.com/gpu-mode/discord-cluster-manager), otherwise, just set DISCORD_CLUSTER_MANAGER_API_BASE_URL to a dummy url in .env file.

7. Finally, create a .env file in the root directory of your sandbox with
SECRET_KEY, DATABASE_URL, REDIS_URL and DISCORD_CLUSTER_MANAGER_API_BASE_URL URL entries. The secret key can be
anything you like; `dev` will work well.

```env
SECRET_KEY=dev
DATABASE_URL=postgresql://user:password@host:port/kernelboard
REDIS_URL=redis://localhost:6379
DISCORD_CLUSTER_MANAGER_API_BASE_URL=http://localhost:8080
```

## Running tests
Expand Down Expand Up @@ -154,3 +157,16 @@ cd frontend && npm run dev
3. Open the React dev server (e.g. `http://localhost:5173/kb/about`) in your browser.

> In this mode, the React app is served separately with hot-reloading. Use it for faster iteration during development.

### Test submission
we pass the submission job to [discord-cluster-manager](https://github.com/gpu-mode/discord-cluster-manager), which will run the job and return the result to the gpumode backend. To test locally end-to-end, you should follow the instructions in the [discord-cluster-manager](https://github.com/gpu-mode/discord-cluster-manager) repo to set up the server locally.

then run the server:
```bash
python src/kernelbot/main.py --debug
```
and pass the url to your .env file:
```env
DISCORD_CLUSTER_MANAGER_API_BASE_URL=http://localhost:8080
```
Please notice, you need to make sure both of them connects to same db instance.
2 changes: 1 addition & 1 deletion frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Assume you have a toggle component that can be expanded by user click.
```
// ToggleShowMore.tsx
export function ToggleShowMore() {
const [expanded, setExpanded] = useState(false);
const [expanded, setExpanded] = useState(true);

return (
<Box sx={{ textAlign: "center", py: 1 }}>
Expand Down
46 changes: 46 additions & 0 deletions frontend/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,49 @@ export async function logout(): Promise<any> {
const r = await res.json();
return r.data;
}

export async function submitFile(form: FormData) {
const resp = await fetch("/api/submission", {
method: "POST",
body: form,
});

const text = await resp.text();
let data: any;
try {
data = JSON.parse(text);
} catch {
data = { raw: text };
}

if (!resp.ok) {
const msg = data?.detail || data?.message || "Submission failed";
throw new Error(msg);
}

return data; // e.g. { submission_id, message, ... }
}

export async function fetchUserSubmissions(
leaderboardId: number | string,
userId: number | string,
page: number = 1,
pageSize: number = 10,
): Promise<any> {
const offset = (page - 1) * pageSize;
const res = await fetch(
`/api/submissions?leaderboard_id=${leaderboardId}&offset=${offset}&limit=${pageSize}`,
);
if (!res.ok) {
let message = "Unknown error";
try {
const json = await res.json();
message = json?.detail || json?.message || message;
} catch {
/* ignore */
}
throw new APIError(`Failed to fetch submissions: ${message}`, res.status);
}
const r = await res.json();
return r.data;
}
1 change: 1 addition & 0 deletions frontend/src/components/app-layout/NavUserProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default function NavUserProfile() {
const logoutAndRefresh = useAuthStore((s) => s.logoutAndRefresh);

const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);

const [notification, setNotification] = useState<{
open: boolean;
message: string;
Expand Down
91 changes: 27 additions & 64 deletions frontend/src/components/codeblock/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,60 +14,27 @@ import ExpandLessIcon from "@mui/icons-material/ExpandLess";

interface CodeBlockProps {
code: string;
maxHeight?: number;
maxHeight?: number | string;
}

export const styles = {
const styles = {
container: {
position: "relative",
border: "1px solid #ddd",
borderRadius: 2,
bgcolor: "#f9f9f9",
fontFamily: "monospace",
overflow: "hidden",
},

copyButton: {
position: "absolute",
top: 8,
right: 8,
top: 4,
right: 4,
zIndex: 1,
},

toggleText: {
cursor: "pointer",
color: "primary.main",
display: "inline-flex",
alignItems: "center",
userSelect: "none",
},

fadeOverlay: (theme: Theme): SxProps<Theme> => ({
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 48,
background: `linear-gradient(to bottom, rgba(249,249,249,0), ${theme.palette.background.paper})`,
pointerEvents: "none",
}),

prestyle(expanded: boolean, maxHeight: number): SxProps<Theme> {
return {
m: 0,
px: 2,
py: 2,
maxHeight: expanded ? "none" : `${maxHeight}px`,
overflowX: "auto",
overflowY: expanded ? "visible" : "hidden",
whiteSpace: "pre",
position: "relative",
};
pre: {
fontFamily: "monospace",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
},
};

export default function CodeBlock({ code, maxHeight = 160 }: CodeBlockProps) {
const [expanded, setExpanded] = useState(false);
export default function CodeBlock({ code }: CodeBlockProps) {
const [copied, setCopied] = useState(false);
const theme = useTheme();

Expand All @@ -78,8 +45,6 @@ export default function CodeBlock({ code, maxHeight = 160 }: CodeBlockProps) {
});
};

// dynamically render the pre based on the expanded state

return (
<Box sx={styles.container}>
{/* Copy Button */}
Expand All @@ -91,27 +56,25 @@ export default function CodeBlock({ code, maxHeight = 160 }: CodeBlockProps) {
</Tooltip>
</Box>

{/* Code */}
<Box component="pre" sx={styles.prestyle(expanded, maxHeight)}>
{/* Scrollable Code */}
<Box
component="pre"
sx={{
...styles.pre,
maxHeight: {
xs: "40vh", // mobile
sm: "50vh", // ipad
md: "60vh", // desktop
},
overflowY: "auto", // pass maxHeight to overflowY to enable scrolling
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2,
p: 1.5,
fontSize: "0.85rem",
bgcolor: theme.palette.background.paper,
}}
>
<code>{code}</code>
{!expanded && <Box sx={styles.fadeOverlay(theme)} />}
</Box>

{/* Toggle */}
<Box sx={{ textAlign: "center", py: 1 }}>
<Typography
data-testid="codeblock-show-all-toggle"
variant="body2"
sx={styles.toggleText}
onClick={() => setExpanded((e) => !e)}
>
{expanded ? "Hide" : "Show more"}
{expanded ? (
<ExpandLessIcon fontSize="small" />
) : (
<ExpandMoreIcon fontSize="small" />
)}
</Typography>
</Box>
</Box>
);
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/common/LoadingCircleProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CircularProgress } from "@mui/material";

export default function LoadingCircleProgress({
message = "loading...",
}: {
message: string;
}) {
return (
<>
<CircularProgress size={18} sx={{ mr: 1 }} />
{message}
</>
);
}
10 changes: 8 additions & 2 deletions frontend/src/components/common/loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,20 @@ const styles = {
},
};

export default function Loading() {
type LoadingProps = {
message?: string;
};

export default function Loading({
message = "Summoning data at lightning speed...",
}: LoadingProps) {
return (
<Box sx={styles.root}>
<CircularProgress color="secondary" />
<Box sx={styles.iconRow}>
<GiRabbit size={40} color="#f48fb1" />
<Typography variant="subtitle1" sx={styles.text}>
Summoning data at lightning speed...
{message}
</Typography>
<HiLightningBolt size={36} color="#ffd54f" />
</Box>
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/lib/date/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,20 @@ export const getTimeLeft = (deadline: string): string => {

return `${days} ${dayLabel} ${hours} ${hourLabel} remaining`;
};

export const isExpired = (
deadline: string | Date,
time: Date = new Date(),
): boolean => {
let d: Date;
if (typeof deadline === "string") {
const parsed = new Date(deadline);
if (isNaN(parsed.getTime())) {
return true;
}
d = parsed;
} else {
d = deadline;
}
return d.getTime() <= time.getTime();
};
1 change: 0 additions & 1 deletion frontend/src/lib/hooks/useApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useCallback, useState } from "react";
import { APIError } from "../../api/api";
import { useNavigate } from "react-router-dom";

type Fetcher<T, Args extends any[]> = (...args: Args) => Promise<T>;
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/lib/types/mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const SubmissionMode = {
TEST: "test",
BENCHMARK: "benchmark",
PROFILE: "profile",
LEADERBOARD: "leaderboard",
PRIVATE: "private",
} as const;

export type SubmissionMode =
(typeof SubmissionMode)[keyof typeof SubmissionMode];
Loading