150 lines
4.8 KiB
TypeScript
150 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useTransition } from "react";
|
|
import { Sparkles, ArrowRight } from "lucide-react";
|
|
|
|
import { BriefPreviewPublic } from "@/components/intake/brief-preview-public";
|
|
import { MAX_QUESTIONS, type Turn } from "@/lib/intake/turn";
|
|
import type { InterviewStep } from "@/lib/intake/interview";
|
|
import type { WorkAreaRecord } from "@/lib/pb/types";
|
|
|
|
/**
|
|
* The public route's interview flow.
|
|
*
|
|
* Mirrors IntakeWizard's shape (a "Start" button, then a question-and-answer
|
|
* loop, then a brief), but calls the public Server Action and renders the
|
|
* public BriefPreview. There is no fallback to the plain request box — the
|
|
* spec deliberately does not offer one on the public route, so an
|
|
* unavailable assistant shows a static message instead.
|
|
*/
|
|
export function IntakePublic({
|
|
projectId,
|
|
projectName,
|
|
token,
|
|
areas,
|
|
}: {
|
|
projectId: string;
|
|
projectName: string;
|
|
token: string;
|
|
areas: WorkAreaRecord[];
|
|
}) {
|
|
const [turns, setTurns] = useState<Turn[]>([]);
|
|
const [question, setQuestion] = useState<string | null>(null);
|
|
const [answer, setAnswer] = useState("");
|
|
const [step, setStep] = useState<InterviewStep | null>(null);
|
|
const [pending, startTransition] = useTransition();
|
|
|
|
async function callAction(nextTurns: Turn[], finishNow: boolean): Promise<InterviewStep> {
|
|
const { intakeStepPublicAction } = await import("@/app/actions/intake-public");
|
|
return intakeStepPublicAction({
|
|
token,
|
|
projectId,
|
|
turns: nextTurns,
|
|
finishNow,
|
|
});
|
|
}
|
|
|
|
function advance(nextTurns: Turn[], finishNow: boolean) {
|
|
startTransition(async () => {
|
|
const result = await callAction(nextTurns, finishNow);
|
|
setStep(result);
|
|
|
|
if (result.kind === "question") {
|
|
setQuestion(result.question ?? null);
|
|
setAnswer("");
|
|
}
|
|
});
|
|
}
|
|
|
|
if (step?.kind === "brief" && step.brief) {
|
|
return (
|
|
<BriefPreviewPublic
|
|
brief={step.brief}
|
|
turns={turns}
|
|
token={token}
|
|
projectId={projectId}
|
|
areas={areas}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (step?.kind === "unavailable") {
|
|
return (
|
|
<section className="space-y-2 rounded-[var(--radius-tile)] border border-border-subtle bg-surface p-4">
|
|
<p className="text-sm text-text-secondary">
|
|
{step.message ?? "The assistant is unavailable right now."}
|
|
</p>
|
|
<p className="text-[11px] text-text-muted">
|
|
The developer is notified of every submission. Please try again
|
|
later, or contact them directly.
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
if (!question) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
disabled={pending}
|
|
onClick={() => advance([], false)}
|
|
className="inline-flex items-center gap-1.5 rounded-lg bg-brand px-3 py-1.5 text-sm font-medium text-brand-fg hover:bg-brand-hover disabled:opacity-50"
|
|
>
|
|
<Sparkles size={14} strokeWidth={2} />
|
|
{pending ? "Starting…" : `Tell ${projectName} what's not working`}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section className="space-y-3 rounded-[var(--radius-tile)] border border-border-subtle bg-surface p-4">
|
|
<div className="flex items-baseline gap-2">
|
|
<span className="text-[11px] uppercase tracking-wide text-text-muted">
|
|
Question {turns.length + 1} of at most {MAX_QUESTIONS}
|
|
</span>
|
|
</div>
|
|
|
|
<p className="text-sm font-medium text-text-primary">{question}</p>
|
|
|
|
<textarea
|
|
autoFocus
|
|
rows={3}
|
|
value={answer}
|
|
onChange={(event) => setAnswer(event.target.value)}
|
|
placeholder="In your own words"
|
|
className="w-full rounded-lg border border-border-subtle bg-panel p-3 text-sm text-text-primary outline-none focus:border-brand"
|
|
/>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
disabled={pending || !answer.trim()}
|
|
onClick={() => {
|
|
const next = [...turns, { question, answer: answer.trim() }];
|
|
setTurns(next);
|
|
advance(next, false);
|
|
}}
|
|
className="inline-flex items-center gap-1.5 rounded-lg bg-brand px-3 py-1.5 text-sm font-medium text-text-fg hover:bg-brand-hover disabled:opacity-50"
|
|
>
|
|
<ArrowRight size={14} strokeWidth={2} />
|
|
{pending ? "…" : "Next"}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
disabled={pending || (turns.length === 0 && !answer.trim())}
|
|
onClick={() => {
|
|
const next = answer.trim()
|
|
? [...turns, { question, answer: answer.trim() }]
|
|
: turns;
|
|
setTurns(next);
|
|
advance(next, true);
|
|
}}
|
|
className="text-xs text-text-muted hover:text-text-secondary disabled:opacity-50"
|
|
>
|
|
That's enough — write it up
|
|
</button>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|