mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 16:43:36 +00:00
introduced supabase and experiment management UI (#23)
* introduced supabase and experiment management UI * fixing cookie import
This commit is contained in:
committed by
GitHub
parent
ab8b8787a8
commit
894ce87a5d
@@ -1,20 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSession } from '@/hooks/useSession';
|
||||
import { TaskManager } from '@/components/admin/TaskManager';
|
||||
import { ExperimentForm } from '@/components/admin/ExperimentForm';
|
||||
|
||||
type Experiment = {
|
||||
id: string;
|
||||
status: 'active' | 'stopped';
|
||||
sessionIds: string[];
|
||||
createdAt: number;
|
||||
subject_name: string;
|
||||
xp_human_only: boolean;
|
||||
xp_market_mode: string;
|
||||
created_at: string;
|
||||
task?: {
|
||||
id: string;
|
||||
task_name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function ExperimentsAdmin() {
|
||||
const { sessionId, isLoading: sessionLoading } = useSession();
|
||||
const [exps, setExps] = useState<Experiment[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedTaskId, setSelectedTaskId] = useState<string | undefined>();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
const fetchExps = async () => {
|
||||
try {
|
||||
@@ -31,86 +37,22 @@ export default function ExperimentsAdmin() {
|
||||
fetchExps();
|
||||
}, []);
|
||||
|
||||
const handleStart = async () => {
|
||||
if (!sessionId) {
|
||||
setError('no session available');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/admin/experiments/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || 'start failed');
|
||||
}
|
||||
|
||||
await fetchExps(); // refresh list
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
const handleExperimentCreated = async () => {
|
||||
setShowForm(false);
|
||||
setSelectedTaskId(undefined);
|
||||
await fetchExps();
|
||||
};
|
||||
|
||||
const handleStop = async (expId: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/admin/experiments/stop', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ experimentId: expId }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || 'stop failed');
|
||||
}
|
||||
|
||||
await fetchExps(); // refresh list
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (sessionLoading) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black">
|
||||
<p className="text-zinc-600 dark:text-zinc-400">loading session...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-50 px-6 py-12 dark:bg-black">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold tracking-tight text-black dark:text-zinc-50">
|
||||
Experiments
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
current session: {sessionId || 'none'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleStart}
|
||||
disabled={loading || !sessionId}
|
||||
className="rounded-lg bg-black px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-800 disabled:opacity-50 dark:bg-zinc-50 dark:text-black dark:hover:bg-zinc-200"
|
||||
>
|
||||
{loading ? 'starting...' : 'start experiment'}
|
||||
</button>
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-semibold tracking-tight text-black dark:text-zinc-50">
|
||||
Experiment Management
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
configure tasks and run experiments
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@@ -119,79 +61,123 @@ export default function ExperimentsAdmin() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-hidden rounded-lg border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<tr>
|
||||
<th className="px-6 py-3 font-medium text-zinc-900 dark:text-zinc-100">
|
||||
experiment id
|
||||
</th>
|
||||
<th className="px-6 py-3 font-medium text-zinc-900 dark:text-zinc-100">
|
||||
status
|
||||
</th>
|
||||
<th className="px-6 py-3 font-medium text-zinc-900 dark:text-zinc-100">
|
||||
session count
|
||||
</th>
|
||||
<th className="px-6 py-3 font-medium text-zinc-900 dark:text-zinc-100">
|
||||
created
|
||||
</th>
|
||||
<th className="px-6 py-3 font-medium text-zinc-900 dark:text-zinc-100">
|
||||
action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-200 dark:divide-zinc-800">
|
||||
{exps.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={5}
|
||||
className="px-6 py-8 text-center text-zinc-500 dark:text-zinc-400"
|
||||
>
|
||||
no experiments yet
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
exps.map((exp) => (
|
||||
<tr
|
||||
key={exp.id}
|
||||
className="hover:bg-zinc-50 dark:hover:bg-zinc-900"
|
||||
>
|
||||
<td className="px-6 py-4 font-mono text-xs text-zinc-700 dark:text-zinc-300">
|
||||
{exp.id.slice(0, 8)}...
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span
|
||||
className={`inline-block rounded-full px-2 py-1 text-xs font-medium ${
|
||||
exp.status === 'active'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-950 dark:text-green-200'
|
||||
: 'bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200'
|
||||
}`}
|
||||
>
|
||||
{exp.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-zinc-700 dark:text-zinc-300">
|
||||
{exp.sessionIds.length}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-zinc-700 dark:text-zinc-300">
|
||||
{new Date(exp.createdAt).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{exp.status === 'active' && (
|
||||
<button
|
||||
onClick={() => handleStop(exp.id)}
|
||||
disabled={loading}
|
||||
className="text-sm font-medium text-red-600 hover:text-red-700 disabled:opacity-50 dark:text-red-400 dark:hover:text-red-300"
|
||||
>
|
||||
stop
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* left column: task manager */}
|
||||
<div className="lg:col-span-1">
|
||||
<TaskManager
|
||||
onTaskSelect={setSelectedTaskId}
|
||||
selectedTaskId={selectedTaskId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* right column: experiment form + list */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
Experiments
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
className="rounded-lg bg-black px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-800 dark:bg-zinc-50 dark:text-black dark:hover:bg-zinc-200"
|
||||
>
|
||||
{showForm ? 'hide form' : 'new experiment'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
<ExperimentForm
|
||||
selectedTaskId={selectedTaskId}
|
||||
onSuccess={handleExperimentCreated}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="overflow-hidden rounded-lg border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">
|
||||
subject
|
||||
</th>
|
||||
<th className="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">
|
||||
mode
|
||||
</th>
|
||||
<th className="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">
|
||||
human
|
||||
</th>
|
||||
<th className="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">
|
||||
task
|
||||
</th>
|
||||
<th className="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">
|
||||
created
|
||||
</th>
|
||||
<th className="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">
|
||||
link
|
||||
</th>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-200 dark:divide-zinc-800">
|
||||
{exps.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={6}
|
||||
className="px-4 py-8 text-center text-zinc-500 dark:text-zinc-400"
|
||||
>
|
||||
no experiments yet
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
exps.map((exp) => {
|
||||
const baseUrl = exp.xp_market_mode === 'airline'
|
||||
? 'https://phantom-airline.vercel.app'
|
||||
: 'https://phantom-hotel.vercel.app';
|
||||
const link = `${baseUrl}/start-task?uuid=${exp.id}`;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={exp.id}
|
||||
className="hover:bg-zinc-50 dark:hover:bg-zinc-900"
|
||||
>
|
||||
<td className="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{exp.subject_name}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="inline-block rounded-full bg-zinc-100 px-2 py-1 text-xs font-medium text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200">
|
||||
{exp.xp_market_mode || 'none'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{exp.xp_human_only ? (
|
||||
<span className="text-xs text-green-600 dark:text-green-400">
|
||||
yes
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-zinc-500">no</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-xs text-zinc-600 dark:text-zinc-400">
|
||||
{exp.task ? exp.task.task_name : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-xs text-zinc-600 dark:text-zinc-400">
|
||||
{new Date(exp.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(link);
|
||||
}}
|
||||
className="text-xs font-medium text-zinc-900 hover:text-zinc-600 dark:text-zinc-100 dark:hover:text-zinc-400"
|
||||
>
|
||||
copy link
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,40 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getAllExperiments } from '@/lib/sessionStore';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/utils/supabase/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const exps = getAllExperiments();
|
||||
return NextResponse.json({ experiments: exps });
|
||||
const cookieStore = await cookies();
|
||||
const supabase = createClient(cookieStore);
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get('id');
|
||||
|
||||
if (id) {
|
||||
const { data, error } = await supabase
|
||||
.from('experiments')
|
||||
.select(`
|
||||
*,
|
||||
task:tasks(*)
|
||||
`)
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return NextResponse.json({ experiment: data });
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('experiments')
|
||||
.select(`
|
||||
*,
|
||||
task:tasks(*)
|
||||
`)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return NextResponse.json({ experiments: data || [] });
|
||||
} catch (err: any) {
|
||||
console.error('experiments list error:', err);
|
||||
return NextResponse.json(
|
||||
@@ -13,3 +43,44 @@ export async function GET() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const supabase = createClient(cookieStore);
|
||||
const body = await req.json();
|
||||
|
||||
const { subject_name, xp_human_only, xp_market_mode, xp_task_id } = body;
|
||||
|
||||
if (!subject_name) {
|
||||
return NextResponse.json(
|
||||
{ error: 'subject_name is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('experiments')
|
||||
.insert([{
|
||||
subject_name,
|
||||
xp_human_only: xp_human_only ?? false,
|
||||
xp_market_mode: xp_market_mode || null,
|
||||
xp_task_id: xp_task_id || null,
|
||||
}])
|
||||
.select(`
|
||||
*,
|
||||
task:tasks(*)
|
||||
`)
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return NextResponse.json({ experiment: data });
|
||||
} catch (err: any) {
|
||||
console.error('experiment creation error:', err);
|
||||
return NextResponse.json(
|
||||
{ error: err.message || 'unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
58
web/src/app/api/admin/tasks/route.ts
Normal file
58
web/src/app/api/admin/tasks/route.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/utils/supabase/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const supabase = createClient(cookieStore);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('tasks')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return NextResponse.json({ tasks: data || [] });
|
||||
} catch (err: any) {
|
||||
console.error('tasks fetch error:', err);
|
||||
return NextResponse.json(
|
||||
{ error: err.message || 'unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const supabase = createClient(cookieStore);
|
||||
const body = await req.json();
|
||||
|
||||
const { task_name, task_description, task_def_of_done } = body;
|
||||
|
||||
if (!task_name) {
|
||||
return NextResponse.json(
|
||||
{ error: 'task_name is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('tasks')
|
||||
.insert([{ task_name, task_description, task_def_of_done }])
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return NextResponse.json({ task: data });
|
||||
} catch (err: any) {
|
||||
console.error('task creation error:', err);
|
||||
return NextResponse.json(
|
||||
{ error: err.message || 'unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { getSession, createSession } from '@/lib/sessionStore';
|
||||
import { getSession, createSession, setExperiment } from '@/lib/sessionStore';
|
||||
|
||||
const COOKIE_NAME = 'phantom_session_id';
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
// check for existing session cookie
|
||||
const existingSession = req.cookies.get(COOKIE_NAME)?.value;
|
||||
|
||||
if (existingSession) {
|
||||
@@ -18,13 +17,11 @@ export async function GET(req: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
// mint new session id
|
||||
const sessionId = randomUUID();
|
||||
createSession(sessionId);
|
||||
|
||||
const res = NextResponse.json({ sessionId, experimentId: undefined });
|
||||
|
||||
// set httpOnly cookie with security flags
|
||||
res.cookies.set({
|
||||
name: COOKIE_NAME,
|
||||
value: sessionId,
|
||||
@@ -32,7 +29,7 @@ export async function GET(req: NextRequest) {
|
||||
sameSite: 'lax',
|
||||
secure: isProd,
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 30, // 30 days
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
});
|
||||
|
||||
return res;
|
||||
@@ -44,3 +41,52 @@ export async function GET(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { experimentId } = body;
|
||||
|
||||
if (!experimentId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'experimentId is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
let sessionId = req.cookies.get(COOKIE_NAME)?.value;
|
||||
|
||||
if (!sessionId) {
|
||||
sessionId = randomUUID();
|
||||
createSession(sessionId);
|
||||
}
|
||||
|
||||
setExperiment(sessionId, experimentId);
|
||||
|
||||
const res = NextResponse.json({
|
||||
sessionId,
|
||||
experimentId,
|
||||
success: true
|
||||
});
|
||||
|
||||
if (!req.cookies.get(COOKIE_NAME)) {
|
||||
res.cookies.set({
|
||||
name: COOKIE_NAME,
|
||||
value: sessionId,
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: isProd,
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (err: any) {
|
||||
console.error('session update error:', err);
|
||||
return NextResponse.json(
|
||||
{ error: err.message || 'unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
93
web/src/app/start-task/page.tsx
Normal file
93
web/src/app/start-task/page.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, Suspense } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
|
||||
const StartTaskContent = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const [status, setStatus] = useState<'loading' | 'error' | 'redirecting'>('loading');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const uuid = searchParams.get('uuid');
|
||||
|
||||
if (!uuid) {
|
||||
setError('no experiment UUID provided');
|
||||
setStatus('error');
|
||||
return;
|
||||
}
|
||||
|
||||
const validateAndStore = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/admin/experiments?id=${uuid}`);
|
||||
if (!res.ok) throw new Error('experiment not found');
|
||||
|
||||
const data = await res.json();
|
||||
const exp = data.experiment;
|
||||
|
||||
if (!exp) throw new Error('invalid experiment UUID');
|
||||
|
||||
localStorage.setItem('phantom_experiment_id', uuid);
|
||||
|
||||
await fetch('/api/session', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ experimentId: uuid }),
|
||||
});
|
||||
|
||||
setStatus('redirecting');
|
||||
|
||||
setTimeout(() => {
|
||||
router.push("/");
|
||||
}, 800);
|
||||
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'failed to start task');
|
||||
setStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
validateAndStore();
|
||||
}, [searchParams, router]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black">
|
||||
<div className="text-center">
|
||||
{status === 'loading' && (
|
||||
<div>
|
||||
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-zinc-200 border-t-zinc-900 dark:border-zinc-800 dark:border-t-zinc-100 mx-auto" />
|
||||
<p className="text-zinc-600 dark:text-zinc-400">validating browser...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'redirecting' && (
|
||||
<div>
|
||||
<div className="mb-4 text-4xl">✓</div>
|
||||
<p className="text-zinc-900 dark:text-zinc-100 font-medium">website loaded</p>
|
||||
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-400">redirecting to page...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<div className="rounded-lg bg-red-50 p-6 dark:bg-red-950">
|
||||
<p className="text-red-900 dark:text-red-100 font-medium">error</p>
|
||||
<p className="mt-2 text-sm text-red-700 dark:text-red-300">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function StartTaskPage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black">
|
||||
<p className="text-zinc-600 dark:text-zinc-400">loading...</p>
|
||||
</div>
|
||||
}>
|
||||
<StartTaskContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user