Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stateful logs + Progress page #42

Merged
merged 19 commits into from
Jan 26, 2024
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
5 changes: 2 additions & 3 deletions web/app/strategy/[id]/page.tsx → web/app/r/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export default async function StrategyPage({
}: {
params: { id: string };
}) {
const workerId = params.id;
const supabase = createSupabaseServerClient();

// // Fetch the runs for this worker
Expand All @@ -25,13 +24,13 @@ export default async function StrategyPage({
)
`
)
.eq("worker_id", workerId)
.eq("id", params.id)
.order("created_at", { ascending: false })
.single();

if (runs.error || !runs.data) {
console.error(runs.error);
throw Error(`Runs with worker_id ${workerId} not found.`);
throw Error(`Runs with id ${params.id} not found.`);
}

const data = runs.data.strategy_entries as unknown as StrategyWithProjects;
Expand Down
40 changes: 40 additions & 0 deletions web/app/r/[id]/progress/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Logs from "@/components/Logs";
import TextField from "@/components/TextField";
import { createSupabaseServerClient } from "@/utils/supabase-server";

async function PromptField(props: { runId: string }) {
const supabase = createSupabaseServerClient()

const { data: run } = await supabase.from('runs').select(`
id,
prompt
`).eq("id", props.runId).single()

if (!run) {
throw new Error(`Run with ID '${props.runId}' not found`)
}

return <TextField
label='Results for'
value={run.prompt}
readOnly={true}
/>
}

export default function ProgressPage(props: {
params: {
id: string
}
}) {
return (
<div className="w-full flex justify-center h-full p-16">
<div className="w-full max-w-3xl flex flex-col gap-8">
<div className="flex flex-col gap-2">
<PromptField runId={props.params.id} />
</div>
<div className="w-full h-[1px] bg-indigo-500" />
<Logs runId={props.params.id} />
</div>
</div>
)
}
5 changes: 3 additions & 2 deletions web/components/LoadingCircle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import clsx from "clsx";
interface LoadingCircleProps {
strokeWidth?: number;
className?: string;
hideText?: boolean
}

const LoadingCircle = ({ strokeWidth = 12, className }: LoadingCircleProps) => {
const LoadingCircle = ({ strokeWidth = 12, className, hideText }: LoadingCircleProps) => {
return (
<div role="status">
<svg
Expand Down Expand Up @@ -34,7 +35,7 @@ const LoadingCircle = ({ strokeWidth = 12, className }: LoadingCircleProps) => {
strokeLinecap="round"
/>
</svg>
<span className="sr-only">Loading...</span>
{ hideText ? <></> : <span className="sr-only">Loading...</span> }
</div>
);
};
Expand Down
30 changes: 30 additions & 0 deletions web/components/Logs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use server"

import { createSupabaseServerClient } from "@/utils/supabase-server";
import RealtimeLogs from "./RealtimeLogs";

export default async function Logs(props: { runId: string }) {
const supabase = createSupabaseServerClient()

const { data: run } = await supabase.from('runs').select(`
id,
prompt,
logs(
id,
run_id,
created_at,
value,
ended_at,
status,
step_name
)
`).eq("id", props.runId).single()

if (!run) {
throw new Error(`Run with ID '${props.runId}' not found`)
}

return (
<RealtimeLogs logs={run.logs} run={run} />
)
}
44 changes: 4 additions & 40 deletions web/components/Prompt.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
"use client";

import { ChangeEvent, useEffect, useState } from "react";
import { ChangeEvent, useState } from "react";
import TextField from "./TextField";
import ChatInputButton from "./ChatInputButton";
import { SparkleIcon } from "./Icons";
import Image from "next/image";
import { startWorker } from "@/app/actions";
import { createSupabaseBrowserClient } from "@/utils/supabase-browser";
import { Tables } from "@/supabase/dbTypes";
import { useRouter } from "next/navigation";
import LoadingCircle from "./LoadingCircle";
import PromptInput from "./PromptInput";
Expand All @@ -25,60 +22,27 @@ const PROMPT_SUGESTIONS = [
export default function Prompt() {
const [prompt, setPrompt] = useState<string>("");
const [isWaiting, setIsWaiting] = useState(false);
const [workerId, setWorkerId] = useState<string>();
const [runId, setRunId] = useState<string>();
const [status, setStatus] = useState<string>();

const router = useRouter();
const supabase = createSupabaseBrowserClient();

const sendPrompt = async (prompt: string) => {
setIsWaiting(true);
try {
const response = await startWorker(prompt);
setWorkerId(response.workerId);
setRunId(response.runId);
router.push(`/r/${response.runId}/progress`)
} finally {
setIsWaiting(false);
}
};

useEffect(() => {
if (runId) {
const channel = supabase
.channel("logs-added")
.on(
"postgres_changes",
{
event: "INSERT",
table: "logs",
schema: "public",
filter: `run_id=eq.${runId}`,
},
(payload: { new: Tables<"logs"> }) => {
if (payload.new.message === "STRATEGY_CREATED") {
router.push(`strategy/${workerId}`);
return;
}
setStatus(payload.new.message);
}
)
.subscribe();

return () => {
supabase.removeChannel(channel);
};
}
}, [workerId, supabase, runId, workerId]);

return (
<>
<div className='mx-auto max-w-screen-md'>
{status ? (
{isWaiting ? (
<div className='w-full'>
<div className='flex flex-col justify-center items-center gap-2'>
<LoadingCircle className='w-[40px] h-[40px] ml-10' />
<div className='flex text-md pl-12 text-center'>{status}</div>
<div className='flex text-md pl-12 text-center'>Loading</div>
</div>
</div>
) : (
Expand Down
142 changes: 142 additions & 0 deletions web/components/RealtimeLogs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"use client"

import { Tables } from "@/supabase/dbTypes";
import { createSupabaseBrowserClient } from "@/utils/supabase-browser";
import clsx from "clsx";
import { useRouter } from "next/navigation";
import { useState, useEffect } from "react";
import LoadingCircle from "./LoadingCircle";
import Button from "./Button";

const UNSTARTED_TEXTS: Record<Tables<"logs">["step_name"], string> = {
FETCH_PROJECTS: "Search for relevant projects",
EVALUATE_PROJECTS: "Evaluate proof of impact",
ANALYZE_FUNDING: "Analyze funding needs",
SYNTHESIZE_RESULTS: "Synthesize results",
}

const LOADING_TEXTS: Record<Tables<"logs">["step_name"], string> = {
FETCH_PROJECTS: "Searching for relevant projects...",
EVALUATE_PROJECTS: "Evaluating proof of impact...",
ANALYZE_FUNDING: "Analyzing funding needs...",
SYNTHESIZE_RESULTS: "Synthesizing results...",
}

const STEPS_ORDER: Record<Tables<"logs">["step_name"], number> = {
FETCH_PROJECTS: 1,
EVALUATE_PROJECTS: 2,
ANALYZE_FUNDING: 3,
SYNTHESIZE_RESULTS: 4,
}

const getLogMessage = (log: Tables<"logs">) => {
switch (log.status) {
case "NOT_STARTED": return UNSTARTED_TEXTS[log.step_name]
case "IN_PROGRESS": return LOADING_TEXTS[log.step_name]
case "COMPLETED": return log.value ?? `Completed: ${UNSTARTED_TEXTS[log.step_name]}`
case "ERRORED": return `Error while ${LOADING_TEXTS[log.step_name].toLowerCase()}`
}
}

const checkIfFinished = (logs: Tables<"logs">[]) => {
const sortedLogs = logs.sort((a, b) => {
return STEPS_ORDER[a.step_name] - STEPS_ORDER[b.step_name]
})
const lastStep = sortedLogs.slice(-1)[0];
const isFinished = lastStep.status === "COMPLETED" && lastStep.step_name === "SYNTHESIZE_RESULTS"

return isFinished
}

export default function RealtimeLogs(props: {
logs: Tables<"logs">[]
run: {
id: string;
prompt: string;
}
}) {
const [logs, setLogs] = useState<Tables<"logs">[]>(props.logs)
const supabase = createSupabaseBrowserClient();
const router = useRouter()

const sortedLogsWithSteps = logs.sort((a, b) => {
return STEPS_ORDER[a.step_name] - STEPS_ORDER[b.step_name]
})

const isFinished = checkIfFinished(sortedLogsWithSteps)

const navigateToStrategy = () => {
router.push(`./`)
}

useEffect(() => {
const channel = supabase
.channel("logs-added")
.on(
"postgres_changes",
{
event: "UPDATE",
table: "logs",
schema: "public",
filter: `run_id=eq.${props.run.id}`,
},
async () => {
const response = await supabase.from("logs").select(`
id,
run_id,
created_at,
value,
ended_at,
status,
step_name
`).eq("run_id", props.run.id)
const updatedLogs = response.data

if (!updatedLogs) {
throw new Error(`Logs for Run with ID '${props.run.id}' not found`)
}

setLogs([...updatedLogs])

if (checkIfFinished(updatedLogs)) {
navigateToStrategy()
return;
}
}
)
.subscribe()

return () => {
supabase.removeChannel(channel);
};
}, [supabase, props.run.id]);

return (
<>
<div className="flex flex-col gap-4">
<p>Results:</p>
<div className="flex flex-col gap-2">
{ sortedLogsWithSteps.map(log => (
<div key={log.id} className={clsx(
"p-4 flex flex-nowrap items-center gap-2 border border-indigo-500 rounded-lg bg-indigo-500/50 cursor-pointer",
log.status === "IN_PROGRESS" ? "text-indigo-50" : ""
)}>
{ log.status === "IN_PROGRESS" ? <LoadingCircle hideText={true} className="!stroke-indigo-500 text-indigo-200" /> : <></>}
<p className={clsx(
"flex-1",
log.status === "NOT_STARTED" ? "text-indigo-50" :
log.status === "IN_PROGRESS" ? "text-indigo-500" :
log.status === "COMPLETED" ? "text-indigo-800" :
""
)}>{ getLogMessage(log) }</p>
<div className="w-6 h-6"></div>
</div>
)) }
</div>
</div>
<Button disabled={!isFinished} onClick={() => navigateToStrategy()}>
View Results
</Button>
</>
)
}
Loading
Loading