Skip to content
Open
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
48 changes: 48 additions & 0 deletions packages/shared/src/graphql/guessWhoQuiz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { gql } from 'graphql-request';
import { gqlClient } from './common';

export interface GuessWhoQuizQAInput {
question: string;
answer: string;
}

export interface GuessWhoQuizNextQuestion {
question: string;
options: string[];
}

export interface GuessWhoQuizFinalPersona {
name: string;
description: string;
tags: string[];
}

export interface GuessWhoQuizStepResult {
nextQuestion: GuessWhoQuizNextQuestion | null;
finalPersona: GuessWhoQuizFinalPersona | null;
}

export const GUESS_WHO_QUIZ_STEP_MUTATION = gql`
mutation GuessWhoQuizStep($history: [GuessWhoQuizQAInput!]!) {
guessWhoQuizStep(history: $history) {
nextQuestion {
question
options
}
finalPersona {
name
description
tags
}
}
}
`;

export const requestGuessWhoQuizStep = async (
history: GuessWhoQuizQAInput[],
): Promise<GuessWhoQuizStepResult> => {
const res = await gqlClient.request<{
guessWhoQuizStep: GuessWhoQuizStepResult;
}>(GUESS_WHO_QUIZ_STEP_MUTATION, { history });
return res.guessWhoQuizStep;
};
6 changes: 6 additions & 0 deletions packages/shared/src/lib/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export enum Origin {
ProfileStack = 'profile stack',
BrandedTag = 'branded tag',
MentionedTool = 'mentioned tool',
GuessWhoQuiz = 'guess who quiz',
}

export enum LogEvent {
Expand Down Expand Up @@ -307,6 +308,11 @@ export enum LogEvent {
StartAiFluencyQuiz = 'start ai fluency quiz',
CompleteAiFluencyQuiz = 'complete ai fluency quiz',
ShareAiFluencyQuiz = 'share ai fluency quiz',
// Guess who quiz
StartGuessWhoQuiz = 'start guess who quiz',
AnswerGuessWhoQuestion = 'answer guess who question',
AnswerGuessWhoLlmQuestion = 'answer guess who llm question',
CompleteGuessWhoQuiz = 'complete guess who quiz',
// Plus subscription
UpgradeSubscription = 'upgrade subscription',
ManageSubscription = 'manage subscription',
Expand Down
131 changes: 131 additions & 0 deletions packages/webapp/components/guess-who/GuessWhoQuiz.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import type { ReactElement } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { AnimatePresence } from 'framer-motion';
import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext';
import { LogEvent, Origin } from '@dailydotdev/shared/src/lib/log';
import type { GuessWhoQuizQAInput } from '@dailydotdev/shared/src/graphql/guessWhoQuiz';
import { LlmPhase } from './LlmPhase';
import { QuestionCard } from './QuestionCard';
import {
FIRST_QUESTION_ID,
TOTAL_VISIBLE_STEPS,
getNextQuestionId,
questions,
} from './questions';

const buildInitialHistory = (
orderedQuestionIds: string[],
answers: Record<string, string>,
): GuessWhoQuizQAInput[] =>
orderedQuestionIds
.map((qid) => {
const question = questions[qid];
const optionId = answers[qid];
const option = question?.options.find((opt) => opt.id === optionId);
if (!question || !option) {
return null;
}
return { question: question.prompt, answer: option.label };
})
.filter((entry): entry is GuessWhoQuizQAInput => entry !== null);

export const GuessWhoQuiz = (): ReactElement => {
const { logEvent } = useLogContext();
const [currentId, setCurrentId] = useState<string>(FIRST_QUESTION_ID);
const [answers, setAnswers] = useState<Record<string, string>>({});
const [history, setHistory] = useState<string[]>([]);
const [isComplete, setIsComplete] = useState(false);
const didLogStart = useRef(false);

useEffect(() => {
if (didLogStart.current) {
return;
}
didLogStart.current = true;
logEvent({
event_name: LogEvent.StartGuessWhoQuiz,
origin: Origin.GuessWhoQuiz,
});
}, [logEvent]);

const reset = useCallback(() => {
setCurrentId(FIRST_QUESTION_ID);
setAnswers({});
setHistory([]);
setIsComplete(false);
}, []);

const handleSelect = useCallback(
(optionId: string) => {
const question = questions[currentId];
if (!question) {
throw new Error(`Unknown question id "${currentId}"`);
}
logEvent({
event_name: LogEvent.AnswerGuessWhoQuestion,
target_id: currentId,
origin: Origin.GuessWhoQuiz,
extra: JSON.stringify({ optionId }),
});
const nextId = getNextQuestionId(question, optionId);
setAnswers((prev) => ({ ...prev, [currentId]: optionId }));

if (!nextId) {
setIsComplete(true);
return;
}

setHistory((prev) => [...prev, currentId]);
setCurrentId(nextId);
},
[currentId, logEvent],
);

const handleBack = useCallback(() => {
setHistory((prev) => {
if (prev.length === 0) {
return prev;
}
const next = prev.slice(0, -1);
const previousId = prev[prev.length - 1];
setCurrentId(previousId);
setIsComplete(false);
return next;
});
}, []);

const initialLlmHistory = useMemo(
() =>
isComplete ? buildInitialHistory([...history, currentId], answers) : [],
[isComplete, history, currentId, answers],
);

if (isComplete) {
return <LlmPhase initialHistory={initialLlmHistory} onRestart={reset} />;
}

const currentQuestion = questions[currentId];
if (!currentQuestion) {
throw new Error(`Unknown question id "${currentId}"`);
}

return (
<AnimatePresence mode="wait">
<QuestionCard
key={currentQuestion.id}
question={currentQuestion}
selectedOptionId={answers[currentId]}
step={history.length + 1}
totalSteps={TOTAL_VISIBLE_STEPS}
onSelect={handleSelect}
onBack={history.length > 0 ? handleBack : undefined}
/>
</AnimatePresence>
);
};
104 changes: 104 additions & 0 deletions packages/webapp/components/guess-who/LlmPhase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { ReactElement } from 'react';
import React, { useCallback } from 'react';
import { motion } from 'framer-motion';
import {
Button,
ButtonSize,
ButtonVariant,
} from '@dailydotdev/shared/src/components/buttons/Button';
import { Loader } from '@dailydotdev/shared/src/components/Loader';
import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext';
import { LogEvent, Origin } from '@dailydotdev/shared/src/lib/log';
import type { GuessWhoQuizQAInput } from '@dailydotdev/shared/src/graphql/guessWhoQuiz';
import { useGuessWhoQuiz } from './useGuessWhoQuiz';
import { LlmQuestionCard } from './LlmQuestionCard';
import { PersonaResult } from './PersonaResult';

interface LlmPhaseProps {
initialHistory: GuessWhoQuizQAInput[];
onRestart: () => void;
}

export const LlmPhase = ({
initialHistory,
onRestart,
}: LlmPhaseProps): ReactElement => {
const { logEvent } = useLogContext();
const { nextQuestion, persona, isPending, error, submitAnswer, retry } =
useGuessWhoQuiz(initialHistory);

const onSelectAnswer = useCallback(
(question: string, answer: string) => {
logEvent({
event_name: LogEvent.AnswerGuessWhoLlmQuestion,
origin: Origin.GuessWhoQuiz,
extra: JSON.stringify({ answer }),
});
submitAnswer(question, answer);
},
[logEvent, submitAnswer],
);

if (persona) {
return <PersonaResult persona={persona} onRestart={onRestart} />;
}

if (error) {
return (
<motion.section
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
className="flex w-full max-w-[36rem] flex-col items-center gap-4 rounded-16 border border-border-subtlest-secondary bg-background-subtle p-6 text-center laptop:p-8"
>
<h2 className="font-bold text-text-primary typo-title3">
Something glitched
</h2>
<p className="text-text-tertiary typo-body">
We couldn&apos;t reach our developer-vibes oracle. Try again?
</p>
<div className="flex gap-3">
<Button
type="button"
variant={ButtonVariant.Secondary}
size={ButtonSize.Medium}
onClick={retry}
>
Try again
</Button>
<Button
type="button"
variant={ButtonVariant.Tertiary}
size={ButtonSize.Medium}
onClick={onRestart}
>
Start over
</Button>
</div>
</motion.section>
);
}

if (nextQuestion && !isPending) {
return (
<LlmQuestionCard
question={nextQuestion.question}
options={nextQuestion.options}
onSelect={(answer) => onSelectAnswer(nextQuestion.question, answer)}
/>
);
}

return (
<motion.section
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
className="flex w-full max-w-[36rem] flex-col items-center gap-4 rounded-16 border border-border-subtlest-secondary bg-background-subtle p-6 laptop:p-8"
>
<Loader />
<p className="text-text-tertiary typo-body">
Cooking up your developer persona…
</p>
</motion.section>
);
};
41 changes: 41 additions & 0 deletions packages/webapp/components/guess-who/LlmQuestionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { ReactElement } from 'react';
import React from 'react';
import { motion } from 'framer-motion';

interface LlmQuestionCardProps {
question: string;
options: string[];
onSelect: (answer: string) => void;
}

export const LlmQuestionCard = ({
question,
options,
onSelect,
}: LlmQuestionCardProps): ReactElement => (
<motion.section
key={question}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.18, ease: 'easeOut' }}
className="flex w-full max-w-[36rem] flex-col rounded-16 border border-border-subtlest-secondary bg-background-subtle p-6 laptop:p-8"
>
<p className="mb-3 text-text-tertiary typo-footnote">One more thing…</p>
<h2 className="mb-6 text-center font-bold text-text-primary typo-title2">
{question}
</h2>
<div className="flex flex-col gap-3">
{options.map((option) => (
<button
key={option}
type="button"
onClick={() => onSelect(option)}
className="flex items-center gap-3 rounded-12 border border-border-subtlest-tertiary bg-background-default px-4 py-3 text-left text-text-secondary transition-colors hover:border-text-tertiary hover:text-text-primary"
>
<span className="typo-body">{option}</span>
</button>
))}
</div>
</motion.section>
);
Loading
Loading