Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b6a2895
feat: integrate treasury management features and refactor hackathon o…
0xdevcollins Jun 11, 2026
a0d21b2
chore: update package-lock.json dependencies
0xdevcollins Jun 11, 2026
8f3a14b
chore: update pre-push husky hook to perform full type-checks and pro…
0xdevcollins Jun 11, 2026
49fe90a
refactor: remove DevelopmentStatusModal from global layout
0xdevcollins Jun 11, 2026
3deb64c
feat: task-first Winners page, judging UX, private access, treasury r…
0xdevcollins Jun 16, 2026
0edc1aa
feat(bounty): ModeTab + SubmissionModelTab for the Configure wizard (…
Benjtalkshow Jun 16, 2026
fb527ff
feat(bounty): features/bounties data layer (types, keys, draft + escr…
Benjtalkshow Jun 16, 2026
4c31f6b
feat(bounty): wizard shell + step/draft machinery (#598) (#613)
Benjtalkshow Jun 17, 2026
a3a81a2
feat(bounty): Scope + Reward + Review tabs and Zod schemas (#600) (#614)
Benjtalkshow Jun 17, 2026
3b45ea9
feat(bounty): use-bounty-publish (escrow publish via shared runner) (…
Benjtalkshow Jun 17, 2026
8e71a49
feat(bounty): routes + organization bounty list page (#597) (#616)
Benjtalkshow Jun 17, 2026
40fd942
feat(bounty): sidebar nav entry + Post Bounty quick action (#602) (#617)
Benjtalkshow Jun 17, 2026
cad5635
feat(bounty): wizard enhancements + hackathon-style org bounty list (…
Benjtalkshow Jun 24, 2026
a0f17e0
Feat/crowdfunding (#620)
0xdevcollins Jun 24, 2026
495689f
Feat/crowdfunding (#642)
0xdevcollins Jun 24, 2026
b35875d
Feat/crowdfunding (#644)
0xdevcollins Jun 24, 2026
f48fc4d
Feat/crowdfunding (#648)
0xdevcollins Jun 24, 2026
c917f2b
Feat/crowdfunding (#650)
0xdevcollins Jun 24, 2026
62ce88d
feat: add tabbed navigation to campaign page for overview, milestones…
0xdevcollins Jun 24, 2026
ec2cbcb
refactor: enhance milestone detail page with status-specific feedback…
0xdevcollins Jun 25, 2026
2862bee
feat: add polling to campaign query and improve cache invalidation ac…
0xdevcollins Jun 25, 2026
f117fa7
feat(bounty): participant data layer (escrow client + hooks) (#643)
Benjtalkshow Jun 25, 2026
c621616
feat(bounty): public bounty marketplace (discover/list) (#645)
Benjtalkshow Jun 25, 2026
928aa73
feat(bounty): bounty detail page (/bounties/[id]) (#646)
Benjtalkshow Jun 25, 2026
37aa7d6
feat(bounty): apply / join / claim flow + edit/withdraw (mode-aware) …
Benjtalkshow Jun 25, 2026
c76b521
feat(bounty): submit work (on-chain anchor) + withdraw submission (#649)
Benjtalkshow Jun 25, 2026
d03d1a5
feat(bounty): builder "my bounties" activity + rewards receipt (#651)
Benjtalkshow Jun 25, 2026
6f46ea7
ci: auto-close linked issues on merge to any base branch (#652)
Benjtalkshow Jun 25, 2026
63922cb
feat(bounty): codegen refresh + retire hand-typed participant casts (…
Benjtalkshow Jun 25, 2026
348b8d8
feat: add template selection to hackathon generation dialog with new …
0xdevcollins Jun 25, 2026
53daf07
Merge branch 'feat/t-replace' of github.com:boundlessfi/boundless int…
0xdevcollins Jun 25, 2026
1a8b26c
feat(bounty): submission page, marketplace redesign, and organizer re…
Benjtalkshow Jun 25, 2026
2ca0987
feat: implement AI-assisted draft generation for hackathons and bounties
0xdevcollins Jun 29, 2026
13e09a6
Merge branch 'feat/t-replace' of github.com:boundlessfi/boundless int…
0xdevcollins Jun 29, 2026
e08d06c
merge: resolve origin/main into feat/t-replace (#665)
0xdevcollins Jun 30, 2026
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
89 changes: 89 additions & 0 deletions components/ai/AiAssumptionsBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use client';

import { useState } from 'react';
import { Sparkles, X } from 'lucide-react';

import { BoundlessButton } from '@/components/buttons';

export interface AiAssumption {
section: string;
field: string;
note: string;
}

interface AiAssumptionsBannerProps {
assumptions: AiAssumption[];
/** Jump to the wizard step that owns an assumption so the organizer can edit. */
onReview?: (section: string) => void;
className?: string;
}

/**
* Shows the non-obvious choices an AI draft made ("Assumed a single winner…")
* so the organizer can see and correct every guess. Shared by the bounty and
* hackathon review steps. Dismissible; renders nothing when there's nothing to
* surface.
*/
export default function AiAssumptionsBanner({
assumptions,
onReview,
className,
}: AiAssumptionsBannerProps) {
const [dismissed, setDismissed] = useState(false);
if (dismissed || !assumptions || assumptions.length === 0) return null;

return (
<div
className={[
'border-primary/30 from-primary/10 rounded-xl border bg-gradient-to-r to-transparent p-4',
className ?? '',
].join(' ')}
>
<div className='flex items-start justify-between gap-3'>
<div className='flex items-start gap-3'>
<span className='bg-primary/15 text-primary mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg'>
<Sparkles className='h-4 w-4' />
</span>
<div>
<p className='text-sm font-semibold text-white'>
A few things the AI assumed
</p>
<p className='text-xs text-gray-400'>
Review these guesses and correct anything that doesn&apos;t match
what you meant.
</p>
</div>
</div>
<button
type='button'
aria-label='Dismiss'
onClick={() => setDismissed(true)}
className='text-gray-500 transition-colors hover:text-gray-300'
>
<X className='h-4 w-4' />
</button>
</div>

<ul className='mt-3 space-y-2'>
{assumptions.map((a, i) => (
<li
key={`${a.section}-${a.field}-${i}`}
className='flex items-center justify-between gap-3 rounded-lg bg-black/20 px-3 py-2'
>
<span className='text-sm text-gray-300'>{a.note}</span>
{onReview && (
<BoundlessButton
type='button'
variant='outline'
size='sm'
onClick={() => onReview(a.section)}
>
Review
</BoundlessButton>
)}
</li>
))}
</ul>
</div>
);
}
52 changes: 52 additions & 0 deletions components/ai/AiBriefPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use client';

import { useState } from 'react';
import { ChevronDown, FileText } from 'lucide-react';

interface AiBriefPanelProps {
/** The brief that produced the AI draft (may include folded-in clarify answers). */
brief?: string | null;
className?: string;
}

/**
* Collapsible "Your brief" panel shown beside an AI-generated draft on the review
* step, so the organizer can compare what they asked for against what the AI
* produced. Renders nothing for manually-created drafts. Shared by both wizards.
*/
export default function AiBriefPanel({ brief, className }: AiBriefPanelProps) {
const [open, setOpen] = useState(false);
if (!brief || brief.trim() === '') return null;

return (
<div
className={[
'rounded-xl border border-zinc-800 bg-zinc-900/40',
className ?? '',
].join(' ')}
>
<button
type='button'
onClick={() => setOpen(o => !o)}
className='flex w-full items-center justify-between gap-2 px-4 py-3 text-left'
aria-expanded={open}
>
<span className='flex items-center gap-2 text-sm font-medium text-white'>
<FileText className='h-4 w-4 text-gray-400' />
Your brief
</span>
<ChevronDown
className={[
'h-4 w-4 text-gray-500 transition-transform',
open ? 'rotate-180' : '',
].join(' ')}
/>
</button>
{open && (
<div className='border-t border-zinc-800 px-4 py-3'>
<p className='text-sm whitespace-pre-wrap text-gray-300'>{brief}</p>
</div>
)}
</div>
);
}
130 changes: 130 additions & 0 deletions components/ai/AiClarifyQuestions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use client';

import { useState } from 'react';
import { Sparkles } from 'lucide-react';

import { BoundlessButton } from '@/components/buttons';

export interface ClarifyQuestionOption {
value: string;
label: string;
}
export interface ClarifyQuestionItem {
id: string;
question: string;
options: ClarifyQuestionOption[];
}

/** A picked answer, ready to fold into the brief. */
export interface ClarifyAnswer {
question: string;
label: string;
}

interface AiClarifyQuestionsProps {
questions: ClarifyQuestionItem[];
/** Continue to drafting with the chosen answers (skipped questions omitted). */
onSubmit: (answers: ClarifyAnswer[]) => void;
/** Draft now without answering (the AI picks sensible defaults). */
onSkip: () => void;
isSubmitting?: boolean;
}

/**
* Adaptive clarify step shared by the bounty + hackathon generate dialogs.
* Shows 1-3 chip questions when the brief was too thin; answers are folded back
* into the brief before drafting. Answering is optional — Skip drafts straight
* away.
*/
export default function AiClarifyQuestions({
questions,
onSubmit,
onSkip,
isSubmitting = false,
}: AiClarifyQuestionsProps) {
const [selected, setSelected] = useState<Record<string, string>>({});

const handleContinue = () => {
const answers: ClarifyAnswer[] = questions
.filter(q => selected[q.id])
.map(q => ({
question: q.question,
label:
q.options.find(o => o.value === selected[q.id])?.label ??
selected[q.id],
}));
onSubmit(answers);
};

return (
<div className='space-y-4'>
<div className='flex items-start gap-3'>
<span className='bg-primary/15 text-primary mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg'>
<Sparkles className='h-4 w-4' />
</span>
<p className='text-sm text-gray-300'>
A couple of quick questions so the draft matches what you have in
mind. Answer what you can — you can skip the rest.
</p>
</div>

<div className='space-y-4'>
{questions.map(q => (
<div key={q.id} className='space-y-2'>
<p className='text-sm font-medium text-white'>{q.question}</p>
<div className='flex flex-wrap gap-2'>
{q.options.map(o => {
const on = selected[q.id] === o.value;
return (
<button
key={o.value}
type='button'
onClick={() =>
setSelected(prev =>
prev[q.id] === o.value
? (() => {
const next = { ...prev };
delete next[q.id];
return next;
})()
: { ...prev, [q.id]: o.value }
)
}
className={[
'rounded-full border px-3 py-1 text-xs transition-colors',
on
? 'border-primary bg-primary/15 text-primary'
: 'border-zinc-700 text-zinc-400 hover:border-zinc-600 hover:text-zinc-200',
].join(' ')}
>
{o.label}
</button>
);
})}
</div>
</div>
))}
</div>

<div className='flex justify-between gap-2 pt-2'>
<BoundlessButton
type='button'
variant='outline'
onClick={onSkip}
disabled={isSubmitting}
>
Skip
</BoundlessButton>
<BoundlessButton
type='button'
loading={isSubmitting}
icon={<Sparkles className='h-4 w-4' />}
iconPosition='left'
onClick={handleContinue}
>
Generate draft
</BoundlessButton>
</div>
</div>
);
}
71 changes: 71 additions & 0 deletions components/ai/AiExampleReference.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use client';

import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';

export interface ExampleItem {
id: string;
label: string;
/** The text fed to the model as a style reference when this item is picked. */
example: string;
}

interface AiExampleReferenceProps {
items: ExampleItem[];
/** Selected item id, or null for none. */
value: string | null;
onChange: (id: string | null) => void;
/** e.g. "bounty" | "hackathon". */
noun: string;
}

const NONE = '__none__';

/**
* Optional "use a past one as a style reference" picker for the Generate
* dialogs. The chosen item's gist is passed to the model as `examples[]` so the
* draft mirrors the organizer's house style. Renders nothing when there's
* nothing to reference. Shared by both wizards.
*/
export default function AiExampleReference({
items,
value,
onChange,
noun,
}: AiExampleReferenceProps) {
if (items.length === 0) return null;

return (
<div className='space-y-1.5'>
<p className='text-sm font-medium'>
Match a past {noun}{' '}
<span className='text-muted-foreground font-normal'>(optional)</span>
</p>
<Select
value={value ?? NONE}
onValueChange={v => onChange(v === NONE ? null : v)}
>
<SelectTrigger className='h-10 rounded-lg border-zinc-800 bg-zinc-900/50 text-white'>
<SelectValue placeholder={`Pick a past ${noun} to match its style`} />
</SelectTrigger>
<SelectContent className='border-zinc-800 bg-zinc-950 text-white'>
<SelectItem value={NONE}>None</SelectItem>
{items.map(item => (
<SelectItem key={item.id} value={item.id}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className='text-muted-foreground text-xs'>
We&apos;ll use its style as a reference — your brief still drives the
content.
</p>
</div>
);
}
Loading
Loading