Initial commit for project Spoon!
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQuery } from 'convex/react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';
|
||||
import { api } from '@spoon/backend/convex/_generated/api.js';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Textarea,
|
||||
} from '@spoon/ui';
|
||||
|
||||
const AgentsPage = () => {
|
||||
const spoons = useQuery(api.spoons.listMine, {}) ?? [];
|
||||
const requests = useQuery(api.agentRequests.listRecent, { limit: 50 }) ?? [];
|
||||
const createRequest = useMutation(api.agentRequests.create);
|
||||
const [spoonId, setSpoonId] = useState('');
|
||||
const [targetBranch, setTargetBranch] = useState('');
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (!spoonId) {
|
||||
toast.error('Choose a Spoon first.');
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await createRequest({
|
||||
spoonId: spoonId as Id<'spoons'>,
|
||||
prompt,
|
||||
targetBranch: targetBranch || undefined,
|
||||
});
|
||||
setPrompt('');
|
||||
setTargetBranch('');
|
||||
toast.success('Agent request queued.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('Could not queue agent request.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className='space-y-6'>
|
||||
<div>
|
||||
<h1 className='text-3xl font-semibold tracking-normal'>Agents</h1>
|
||||
<p className='text-muted-foreground mt-2'>
|
||||
Queue prompt-driven work for future AI merge request automation.
|
||||
</p>
|
||||
</div>
|
||||
<div className='grid gap-6 xl:grid-cols-[0.9fr_1.1fr]'>
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle>Request work</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className='space-y-4'>
|
||||
<div className='grid gap-2'>
|
||||
<Label>Spoon</Label>
|
||||
<Select value={spoonId} onValueChange={setSpoonId}>
|
||||
<SelectTrigger className='w-full'>
|
||||
<SelectValue placeholder='Choose a Spoon' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{spoons.map((spoon) => (
|
||||
<SelectItem key={spoon._id} value={spoon._id}>
|
||||
{spoon.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='targetBranch'>Target branch</Label>
|
||||
<Input
|
||||
id='targetBranch'
|
||||
value={targetBranch}
|
||||
placeholder='feature/my-change'
|
||||
onChange={(event) => setTargetBranch(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='prompt'>Prompt</Label>
|
||||
<Textarea
|
||||
id='prompt'
|
||||
value={prompt}
|
||||
required
|
||||
onChange={(event) => setPrompt(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button type='submit' disabled={submitting || !spoons.length}>
|
||||
{submitting ? 'Queueing...' : 'Queue request'}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent requests</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{requests.length ? (
|
||||
<div className='space-y-3'>
|
||||
{requests.map((request) => (
|
||||
<div key={request._id} className='border-border border p-4'>
|
||||
<p className='font-medium'>{request.prompt}</p>
|
||||
<p className='text-muted-foreground mt-1 text-sm'>
|
||||
{request.status.replaceAll('_', ' ')}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-muted-foreground'>
|
||||
Agent requests will appear here after you create a Spoon and
|
||||
queue work.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentsPage;
|
||||
@@ -0,0 +1,121 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { MetricCard } from '@/components/dashboard/metric-card';
|
||||
import { SpoonCard } from '@/components/spoons/spoon-card';
|
||||
import { useQuery } from 'convex/react';
|
||||
import { Bot, GitBranch, GitPullRequest, RefreshCw } from 'lucide-react';
|
||||
|
||||
import { api } from '@spoon/backend/convex/_generated/api.js';
|
||||
import { Button, Card, CardContent, CardHeader, CardTitle } from '@spoon/ui';
|
||||
|
||||
const DashboardPage = () => {
|
||||
const spoons = useQuery(api.spoons.listMine, {}) ?? [];
|
||||
const syncRuns = useQuery(api.syncRuns.listRecent, { limit: 5 }) ?? [];
|
||||
const agentRequests =
|
||||
useQuery(api.agentRequests.listRecent, { limit: 5 }) ?? [];
|
||||
const activeSpoons = spoons.filter(
|
||||
(spoon) => spoon.status === 'active',
|
||||
).length;
|
||||
const needsReview = syncRuns.filter(
|
||||
(run) => run.status === 'needs_review',
|
||||
).length;
|
||||
|
||||
return (
|
||||
<main className='space-y-6'>
|
||||
<div className='flex flex-col justify-between gap-4 md:flex-row md:items-end'>
|
||||
<div>
|
||||
<h1 className='text-3xl font-semibold tracking-normal'>Dashboard</h1>
|
||||
<p className='text-muted-foreground mt-2'>
|
||||
Monitor managed forks, upstream activity, and queued agent work.
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link href='/spoons/new'>Create Spoon</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
||||
<MetricCard
|
||||
label='Total Spoons'
|
||||
value={spoons.length}
|
||||
note='Managed forks'
|
||||
icon={GitBranch}
|
||||
/>
|
||||
<MetricCard
|
||||
label='Active Spoons'
|
||||
value={activeSpoons}
|
||||
note='Ready for checks'
|
||||
icon={GitPullRequest}
|
||||
/>
|
||||
<MetricCard
|
||||
label='Needs review'
|
||||
value={needsReview}
|
||||
note='Upstream updates'
|
||||
icon={RefreshCw}
|
||||
/>
|
||||
<MetricCard
|
||||
label='Agent requests'
|
||||
value={agentRequests.length}
|
||||
note='Queued and recent'
|
||||
icon={Bot}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-6 xl:grid-cols-2'>
|
||||
<section className='space-y-3'>
|
||||
<h2 className='text-lg font-semibold'>Recent Spoons</h2>
|
||||
{spoons.length ? (
|
||||
spoons
|
||||
.slice(0, 3)
|
||||
.map((spoon) => <SpoonCard key={spoon._id} spoon={spoon} />)
|
||||
) : (
|
||||
<Card className='shadow-none'>
|
||||
<CardContent className='p-6'>
|
||||
<p className='font-medium'>No Spoons yet</p>
|
||||
<p className='text-muted-foreground mt-2 text-sm'>
|
||||
Create a manual Spoon record to start shaping your fork
|
||||
maintenance dashboard.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</section>
|
||||
<section className='space-y-3'>
|
||||
<h2 className='text-lg font-semibold'>Recent activity</h2>
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-base'>Upstream checks</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{syncRuns.length ? (
|
||||
<div className='space-y-3'>
|
||||
{syncRuns.map((run) => (
|
||||
<div
|
||||
key={run._id}
|
||||
className='border-border border p-3 text-sm'
|
||||
>
|
||||
<p className='font-medium'>
|
||||
{run.kind.replaceAll('_', ' ')}
|
||||
</p>
|
||||
<p className='text-muted-foreground'>
|
||||
{run.status.replaceAll('_', ' ')}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
Scheduled upstream checks will appear here once provider
|
||||
automation is connected.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardPage;
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { AppShell } from '@/components/app-shell/app-shell';
|
||||
|
||||
const Layout = ({ children }: { children: ReactNode }) => (
|
||||
<AppShell>{children}</AppShell>
|
||||
);
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NewSpoonForm } from '@/components/spoons/new-spoon-form';
|
||||
|
||||
const NewSpoonPage = () => (
|
||||
<main className='space-y-6'>
|
||||
<div>
|
||||
<h1 className='text-3xl font-semibold tracking-normal'>New Spoon</h1>
|
||||
<p className='text-muted-foreground mt-2 max-w-2xl'>
|
||||
Create a provider-neutral managed fork record. This does not call a Git
|
||||
provider yet; it prepares the dashboard surface for future automation.
|
||||
</p>
|
||||
</div>
|
||||
<NewSpoonForm />
|
||||
</main>
|
||||
);
|
||||
|
||||
export default NewSpoonPage;
|
||||
@@ -0,0 +1,49 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { SpoonCard } from '@/components/spoons/spoon-card';
|
||||
import { useQuery } from 'convex/react';
|
||||
|
||||
import { api } from '@spoon/backend/convex/_generated/api.js';
|
||||
import { Button, Card, CardContent } from '@spoon/ui';
|
||||
|
||||
const SpoonsPage = () => {
|
||||
const spoons = useQuery(api.spoons.listMine, {}) ?? [];
|
||||
return (
|
||||
<main className='space-y-6'>
|
||||
<div className='flex flex-col justify-between gap-4 md:flex-row md:items-end'>
|
||||
<div>
|
||||
<h1 className='text-3xl font-semibold tracking-normal'>My Spoons</h1>
|
||||
<p className='text-muted-foreground mt-2'>
|
||||
Managed forks you want to keep close to their upstream projects.
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link href='/spoons/new'>New Spoon</Link>
|
||||
</Button>
|
||||
</div>
|
||||
{spoons.length ? (
|
||||
<div className='grid gap-4 xl:grid-cols-2'>
|
||||
{spoons.map((spoon) => (
|
||||
<SpoonCard key={spoon._id} spoon={spoon} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card className='shadow-none'>
|
||||
<CardContent className='p-8'>
|
||||
<p className='text-lg font-medium'>No managed forks yet</p>
|
||||
<p className='text-muted-foreground mt-2 max-w-xl'>
|
||||
Add your first Spoon manually. Provider-backed forking can build
|
||||
on this same record later.
|
||||
</p>
|
||||
<Button className='mt-5' asChild>
|
||||
<Link href='/spoons/new'>Create Spoon</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpoonsPage;
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import { useQuery } from 'convex/react';
|
||||
|
||||
import { api } from '@spoon/backend/convex/_generated/api.js';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@spoon/ui';
|
||||
|
||||
const UpdatesPage = () => {
|
||||
const runs = useQuery(api.syncRuns.listRecent, { limit: 50 }) ?? [];
|
||||
const spoons = useQuery(api.spoons.listMine, {}) ?? [];
|
||||
return (
|
||||
<main className='space-y-6'>
|
||||
<div>
|
||||
<h1 className='text-3xl font-semibold tracking-normal'>Updates</h1>
|
||||
<p className='text-muted-foreground mt-2'>
|
||||
Upstream checks, merge attempts, and AI reviews will appear here.
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex flex-col gap-3 md:flex-row'>
|
||||
<Select defaultValue='all'>
|
||||
<SelectTrigger className='w-full md:w-48'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='all'>All statuses</SelectItem>
|
||||
<SelectItem value='needs_review'>Needs review</SelectItem>
|
||||
<SelectItem value='conflict'>Conflict</SelectItem>
|
||||
<SelectItem value='clean'>Clean</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select defaultValue='all'>
|
||||
<SelectTrigger className='w-full md:w-64'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='all'>All Spoons</SelectItem>
|
||||
{spoons.map((spoon) => (
|
||||
<SelectItem key={spoon._id} value={spoon._id}>
|
||||
{spoon.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent sync runs</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{runs.length ? (
|
||||
<div className='space-y-3'>
|
||||
{runs.map((run) => (
|
||||
<div key={run._id} className='border-border border p-4'>
|
||||
<p className='font-medium'>{run.kind.replaceAll('_', ' ')}</p>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
{run.status.replaceAll('_', ' ')}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-muted-foreground'>
|
||||
Scheduled upstream checks will appear here once provider
|
||||
connections and workers are added.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdatesPage;
|
||||
Reference in New Issue
Block a user