fix talent pool ui

This commit is contained in:
2026-08-11 17:09:53 +05:30
parent 7427359967
commit 8a256a4de7
12 changed files with 473 additions and 256 deletions

View File

@@ -1,5 +1,7 @@
import React, { useState } from 'react';
import { Lock, Video, Camera, MessageSquare, ScanLine, Loader2, XCircle, CheckCircle2, ShieldCheck, Award, Eye, ArrowRight } from 'lucide-react';
import { Lock, Video, Camera, MessageSquare, ScanLine, XCircle, CheckCircle2, ShieldCheck, Award, Eye, ArrowRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button, Spinner, Surface } from '@/components/ds';
import { isUnlocked } from '@/lib/provingGround';
import { useSubmitChallenge, useVerifyEvidence } from '@/lib/krowHooks';
import RoleplayChallenge from './RoleplayChallenge';
@@ -8,23 +10,43 @@ import VideoRecorder from './VideoRecorder';
import HazardIdentifier from './HazardIdentifier';
import OwliverAvatar from '@/components/krow/OwliverAvatar';
/**
* The challenge itself: learn, demonstrate, verify, proof.
*
* Presentation only — every step, transition and mutation below is the original
* flow. Styling comes from the design system so the runner reads as part of the
* Forge workspace it is embedded in rather than a widget dropped into it.
*/
const STEPS = ['Learn', 'Demonstrate', 'Verify', 'Proof'];
const TYPE_ICON = { roleplay: MessageSquare, photo: Camera, video: Video, photo_identify: ScanLine };
const TYPE_LABEL = { roleplay: 'Chat with Owliver', photo: 'Photo Proof', video: 'Record Yourself', photo_identify: 'Identify Hazards' };
function Stepper({ active }) {
return (
<div className="flex items-center gap-1 mb-6">
<div className="mb-5 flex items-center gap-1.5">
{STEPS.map((s, i) => {
const done = i < active;
const cur = i === active;
return (
<React.Fragment key={s}>
<div className={`flex items-center gap-1.5 ${cur || done ? '' : 'opacity-50'}`}>
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-[11px] font-bold ${done ? 'bg-[#0A39DF] text-white' : cur ? 'bg-[#0A39DF] text-white ring-4 ring-[#EFF6FF]' : 'bg-[#F1F5F9] text-[#94A3B8]'}`}>{done ? '✓' : i + 1}</div>
<span className={`text-[12px] font-medium ${cur || done ? 'text-[#0F172A]' : 'text-[#94A3B8]'}`}>{s}</span>
<div className="flex items-center gap-1.5">
<span
className={cn(
'grid h-5 w-5 place-items-center rounded-full text-[10px] font-bold',
done || cur ? 'bg-krow-blue text-white' : 'bg-surface-sunken text-ink-4',
cur && 'ring-4 ring-krow-blue-tint'
)}
>
{done ? '✓' : i + 1}
</span>
<span className={cn('text-caption font-medium', done || cur ? 'text-ink-1' : 'text-ink-4')}>
{s}
</span>
</div>
{i < STEPS.length - 1 && <div className={`flex-1 h-px ${done ? 'bg-[#0A39DF]' : 'bg-[#E5E7EB]'}`} />}
{i < STEPS.length - 1 && (
<span className={cn('h-px flex-1', done ? 'bg-krow-blue' : 'bg-border')} aria-hidden="true" />
)}
</React.Fragment>
);
})}
@@ -32,6 +54,15 @@ function Stepper({ active }) {
);
}
/** The runner's shell — one surface, one padding, every step. */
function Frame({ children, className }) {
return (
<Surface variant="solid" radius="lg" padding="none" elevation="xs" className={cn('px-5 py-4', className)}>
{children}
</Surface>
);
}
export default function ChallengeRunner({ course, profile, onPassed }) {
const [step, setStep] = useState(0);
const [evaluating, setEvaluating] = useState(false);
@@ -78,62 +109,81 @@ export default function ChallengeRunner({ course, profile, onPassed }) {
// 0 — Learn
if (step === 0) {
return (
<div className="rounded-2xl border border-[#E5E7EB] bg-white p-6">
<Frame>
<Stepper active={0} />
{!lock.unlocked ? (
<>
<div className="flex items-center gap-2 text-[#0F172A] mb-2"><Lock className="w-4 h-4" /><span className="font-semibold text-[15px]">Locked — earn this one</span></div>
<p className="text-[13px] text-[#64748B] mb-3">Real work unlocks learning. Complete these first:</p>
<ul className="space-y-1.5">
{lock.reasons.map((r, i) => (<li key={i} className="text-[13px] text-[#475569] flex items-center gap-2"><span className="w-1.5 h-1.5 rounded-full bg-[#94A3B8]" />{r}</li>))}
<p className="flex items-center gap-2 text-body font-semibold text-ink-1">
<Lock className="h-4 w-4 text-warning" aria-hidden="true" /> Locked — earn this one
</p>
<p className="mt-1 text-body-sm text-ink-3">Real work unlocks learning. Complete these first:</p>
<ul className="mt-2.5 space-y-1.5">
{lock.reasons.map((r, i) => (
<li key={i} className="flex items-start gap-2 text-body-sm text-ink-2">
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-warning" aria-hidden="true" />
{r}
</li>
))}
</ul>
</>
) : (
<>
<div className="flex items-center gap-3 mb-4">
<div className="w-11 h-11 rounded-2xl bg-[#EFF6FF] flex items-center justify-center"><Icon className="w-5 h-5 text-[#0A39DF]" /></div>
<div>
<p className="text-[11px] font-medium uppercase tracking-wide text-[#94A3B8]">Demonstration</p>
<p className="text-[16px] font-semibold text-[#0F172A]">{TYPE_LABEL[type]}</p>
<div className="flex items-center gap-3">
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-krow-blue-tint text-krow-blue">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<div className="min-w-0">
<p className="text-[11px] font-semibold uppercase tracking-wide text-ink-4">Demonstration</p>
<p className="text-body font-semibold text-ink-1">{TYPE_LABEL[type]}</p>
</div>
</div>
<p className="text-[14px] text-[#374151] leading-relaxed mb-4">{ch.prompt || course?.description}</p>
<button onClick={() => setStep(1)} className="w-full h-11 rounded-xl bg-[#0A39DF] hover:bg-[#082EB4] text-white text-[14px] font-semibold flex items-center justify-center gap-2">Show me <ArrowRight className="w-4 h-4" /></button>
<p className="mt-3 max-w-prose text-body-sm leading-relaxed text-ink-2">
{ch.prompt || course?.description}
</p>
<Button shape="rounded" className="mt-4 w-full" onClick={() => setStep(1)}>
Show me <ArrowRight aria-hidden="true" />
</Button>
</>
)}
</div>
</Frame>
);
}
// Evaluating
if (evaluating) {
return (
<div className="rounded-2xl border border-[#E5E7EB] bg-white p-10 flex flex-col items-center">
<Frame className="text-center">
<Stepper active={1} />
<Loader2 className="w-8 h-8 animate-spin text-[#0A39DF] mb-3" />
<p className="text-[14px] font-medium text-[#0F172A]">Owliver is evaluating your proof…</p>
<p className="text-[12px] text-[#64748B] mt-1">Measuring whether you can actually perform.</p>
</div>
<div className="flex flex-col items-center gap-2 py-6">
<Spinner size="lg" />
<p className="text-body-sm font-medium text-ink-1">Owliver is evaluating your proof…</p>
<p className="text-caption text-ink-3">Measuring whether you can actually perform.</p>
</div>
</Frame>
);
}
// 1 — Demonstrate
if (step === 1) {
return (
<div className="rounded-2xl border border-[#E5E7EB] bg-white p-6">
<Frame>
<Stepper active={1} />
<div className="rounded-xl bg-[#F8FAFC] border border-[#F1F5F9] p-3 mb-4 flex items-start gap-2.5">
<OwliverAvatar className="w-7 h-7 shrink-0" rounded="rounded-full" />
<div>
<p className="text-[11px] font-semibold text-[#0A39DF] mb-0.5">Owliver</p>
<p className="text-[13px] text-[#374151]">{ch.prompt || course?.description}</p>
{/* The roleplay widget opens with this same prompt as its first message,
so repeating it above the transcript would say it twice. */}
{type !== 'roleplay' && (
<div className="mb-4 flex items-start gap-2.5 rounded-xl border border-border bg-surface-subtle p-3">
<OwliverAvatar className="h-7 w-7 shrink-0" rounded="rounded-full" />
<div className="min-w-0">
<p className="text-[11px] font-semibold text-krow-blue">Owliver</p>
<p className="text-body-sm leading-relaxed text-ink-2">{ch.prompt || course?.description}</p>
</div>
</div>
</div>
)}
{type === 'roleplay' && <RoleplayChallenge course={course} onSubmit={handleSubmit} submitting={submit.isPending} />}
{type === 'photo' && <MediaChallenge course={course} onSubmit={handleSubmit} submitting={submit.isPending} />}
{type === 'video' && <VideoRecorder onSubmit={handleSubmit} submitting={submit.isPending} />}
{type === 'photo_identify' && <HazardIdentifier course={course} onSubmit={handleSubmit} submitting={submit.isPending} />}
</div>
</Frame>
);
}
@@ -141,69 +191,120 @@ export default function ChallengeRunner({ course, profile, onPassed }) {
if (step === 2) {
const rubric = result?.rubric || {};
return (
<div className="rounded-2xl border border-[#E5E7EB] bg-white p-6">
<Frame>
<Stepper active={2} />
<div className="flex items-center gap-3 mb-4">
{passed ? <CheckCircle2 className="w-9 h-9 text-[#16A34A]" /> : <XCircle className="w-9 h-9 text-[#EF4444]" />}
<div>
<p className="text-[17px] font-semibold text-[#0F172A]">{passed ? 'Verified by Owliver' : 'Needs work'}</p>
<p className="text-[12px] text-[#64748B]">AI confidence: {Math.round(evidence?.ai_score || 0)}/100</p>
<div className="flex items-center gap-3">
{passed
? <CheckCircle2 className="h-8 w-8 shrink-0 text-success" aria-hidden="true" />
: <XCircle className="h-8 w-8 shrink-0 text-destructive" aria-hidden="true" />}
<div className="min-w-0">
<p className="font-heading text-title text-ink-1">{passed ? 'Verified by Owliver' : 'Needs work'}</p>
<p className="text-caption tabular-nums text-ink-3">
AI confidence: {Math.round(evidence?.ai_score || 0)}/100
</p>
</div>
</div>
{result?.feedback && <p className="text-[13px] text-[#374151] leading-relaxed mb-4">{result.feedback}</p>}
{result?.feedback && (
<p className="mt-3 max-w-prose text-body-sm leading-relaxed text-ink-2">{result.feedback}</p>
)}
{Object.keys(rubric).length > 0 && (
<div className="space-y-2 mb-4">
<div className="mt-4 space-y-2">
{Object.entries(rubric).map(([k, v]) => (
<div key={k}>
<div className="flex justify-between text-[12px] mb-1"><span className="text-[#475569] capitalize">{k.replace(/_/g, ' ')}</span><span className="font-semibold text-[#0F172A]">{Math.round(v)}/100</span></div>
<div className="h-1.5 rounded-full bg-[#F1F5F9] overflow-hidden"><div className="h-full bg-[#0A39DF]" style={{ width: `${Math.min(100, Math.round(v))}%` }} /></div>
<div className="mb-1 flex justify-between text-caption">
<span className="capitalize text-ink-3">{k.replace(/_/g, ' ')}</span>
<span className="font-semibold tabular-nums text-ink-1">{Math.round(v)}/100</span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-surface-sunken">
<div className="h-full rounded-full bg-krow-blue" style={{ width: `${Math.min(100, Math.round(v))}%` }} />
</div>
</div>
))}
</div>
)}
<div className={`rounded-xl border p-4 flex items-center gap-3 mb-3 ${supervisorVerified ? 'bg-[#F0FDF4] border-[#BBF7D0]' : 'bg-[#FFFBEB] border-[#FDE68A]'}`}>
<ShieldCheck className={`w-5 h-5 ${supervisorVerified ? 'text-[#16A34A]' : 'text-[#D97706]'}`} />
<div className="flex-1">
<p className="text-[13px] font-medium text-[#0F172A]">{supervisorVerified ? 'Supervisor verified' : 'Supervisor verification'}</p>
<p className="text-[11px] text-[#64748B]">{supervisorVerified ? 'A supervisor confirmed this demonstration in person.' : 'A supervisor can confirm this demonstration on the floor.'}</p>
<div
className={cn(
'mt-4 flex items-center gap-3 rounded-xl border px-3.5 py-3',
supervisorVerified ? 'border-success/30 bg-success-muted' : 'border-warning/30 bg-warning-muted'
)}
>
<ShieldCheck
className={cn('h-4 w-4 shrink-0', supervisorVerified ? 'text-success' : 'text-warning')}
aria-hidden="true"
/>
<div className="min-w-0 flex-1">
<p className="text-body-sm font-medium text-ink-1">
{supervisorVerified ? 'Supervisor verified' : 'Supervisor verification'}
</p>
<p className="text-caption text-ink-3">
{supervisorVerified
? 'A supervisor confirmed this demonstration in person.'
: 'A supervisor can confirm this demonstration on the floor.'}
</p>
</div>
{!supervisorVerified ? (
<button onClick={doVerify} disabled={verify.isPending} className="h-8 px-3 rounded-lg bg-[#0F172A] text-white text-[12px] font-semibold disabled:opacity-50">{verify.isPending ? '…' : 'Verify'}</button>
<Button variant="navy" size="sm" shape="rounded" onClick={doVerify} disabled={verify.isPending}>
{verify.isPending ? '…' : 'Verify'}
</Button>
) : (
<CheckCircle2 className="w-5 h-5 text-[#16A34A]" />
<CheckCircle2 className="h-4 w-4 text-success" aria-hidden="true" />
)}
</div>
{passed ? (
<button onClick={() => setStep(3)} className="w-full h-11 rounded-xl bg-[#0A39DF] text-white text-[14px] font-semibold flex items-center justify-center gap-2">See your proof <ArrowRight className="w-4 h-4" /></button>
<Button shape="rounded" className="mt-3 w-full" onClick={() => setStep(3)}>
See your proof <ArrowRight aria-hidden="true" />
</Button>
) : (
<button onClick={() => setStep(1)} className="w-full h-10 rounded-xl border border-[#E5E7EB] text-[13px] font-semibold text-[#0F172A] hover:bg-[#F8FAFC]">Try the demonstration again</button>
<Button variant="outline" shape="rounded" className="mt-3 w-full" onClick={() => setStep(1)}>
Try the demonstration again
</Button>
)}
</div>
</Frame>
);
}
// 3 — Proof
return (
<div className="rounded-2xl border border-[#E5E7EB] bg-white p-6">
<Frame>
<Stepper active={3} />
<div className="flex items-center gap-3 mb-4">
<div className="w-11 h-11 rounded-2xl bg-[#F9E547] flex items-center justify-center"><Award className="w-5 h-5 text-[#92400E]" /></div>
<div>
<p className="text-[16px] font-semibold text-[#0F172A]">Proof stored</p>
<p className="text-[12px] text-[#64748B]">Capability: {course.proof_skill || course.title}</p>
<div className="flex items-center gap-3">
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-krow-yellow text-krow-blue">
<Award className="h-5 w-5" aria-hidden="true" />
</span>
<div className="min-w-0">
<p className="text-body font-semibold text-ink-1">Proof stored</p>
<p className="text-caption text-ink-3">Capability: {course.proof_skill || course.title}</p>
</div>
</div>
<div className="rounded-xl bg-[#F8FAFC] border border-[#F1F5F9] p-4 space-y-2 mb-4">
<div className="flex justify-between text-[12px]"><span className="text-[#64748B]">AI verdict</span><span className="font-semibold text-[#16A34A] capitalize">{evidence?.ai_verdict}</span></div>
<div className="flex justify-between text-[12px]"><span className="text-[#64748B]">Score</span><span className="font-semibold text-[#0F172A]">{Math.round(evidence?.ai_score || 0)}/100</span></div>
<div className="flex justify-between text-[12px]"><span className="text-[#64748B]">Supervisor</span><span className="font-semibold text-[#0F172A]">{supervisorVerified ? 'Verified' : 'Pending'}</span></div>
<div className="flex justify-between text-[12px]"><span className="text-[#64748B]">Proof type</span><span className="font-semibold text-[#0F172A]">{TYPE_LABEL[type]}</span></div>
</div>
<div className="rounded-xl bg-[#EFF6FF] border border-[#BFDBFE] p-4 flex items-start gap-2.5 mb-4">
<Eye className="w-4 h-4 text-[#0A39DF] mt-0.5 shrink-0" />
<p className="text-[12px] text-[#1E40AF]">Employers can now see this proof in your Career Wallet. Every skill is backed by evidence.</p>
</div>
<button onClick={() => setStep(0)} className="w-full h-10 rounded-xl border border-[#E5E7EB] text-[13px] font-semibold text-[#0F172A] hover:bg-[#F8FAFC]">Back to start</button>
</div>
<dl className="mt-4 divide-y divide-border overflow-hidden rounded-xl border border-border bg-surface-subtle">
{[
{ label: 'AI verdict', value: evidence?.ai_verdict, tone: 'text-success', capitalize: true },
{ label: 'Score', value: `${Math.round(evidence?.ai_score || 0)}/100` },
{ label: 'Supervisor', value: supervisorVerified ? 'Verified' : 'Pending' },
{ label: 'Proof type', value: TYPE_LABEL[type] },
].map((row) => (
<div key={row.label} className="flex items-baseline justify-between gap-3 px-3.5 py-2">
<dt className="text-caption text-ink-3">{row.label}</dt>
<dd className={cn('text-caption font-semibold', row.capitalize && 'capitalize', row.tone || 'text-ink-1')}>
{row.value}
</dd>
</div>
))}
</dl>
<p className="mt-3 flex items-start gap-2 rounded-xl border border-border bg-krow-blue-tint px-3.5 py-3 text-caption leading-relaxed text-krow-blue">
<Eye className="mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true" />
Employers can now see this proof in your Career Wallet. Every skill is backed by evidence.
</p>
<Button variant="outline" shape="rounded" className="mt-3 w-full" onClick={() => setStep(0)}>
Back to start
</Button>
</Frame>
);
}
}

View File

@@ -38,43 +38,43 @@ export default function HazardIdentifier({ course, onSubmit, submitting }) {
return (
<div>
<p className="text-[13px] text-[#64748B] mb-3">Tap anywhere on the kitchen photo to mark a hazard. Label each one, then submit for AI evaluation.</p>
<div className="relative rounded-2xl overflow-hidden border border-[#E5E7EB] mb-3 select-none cursor-crosshair" onClick={addMarker}>
<p className="text-[13px] text-ink-3 mb-3">Tap anywhere on the kitchen photo to mark a hazard. Label each one, then submit for AI evaluation.</p>
<div className="relative rounded-xl overflow-hidden border border-border mb-3 select-none cursor-crosshair" onClick={addMarker}>
{imageUrl ? (
<Image src={imageUrl} alt="Kitchen scene" className="w-full max-h-80 object-cover" fittingType="fill" />
) : (
<div className="w-full h-64 bg-[#F1F5F9] flex items-center justify-center text-[13px] text-[#94A3B8]">No scene image configured</div>
<div className="w-full h-64 bg-surface-sunken flex items-center justify-center text-[13px] text-ink-4">No scene image configured</div>
)}
{markers.map((m, i) => (
<div key={i} className="absolute -translate-x-1/2 -translate-y-1/2 group" style={{ left: `${m.x * 100}%`, top: `${m.y * 100}%` }} onClick={(e) => e.stopPropagation()}>
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-red-500 text-white text-[11px] font-bold shadow ring-2 ring-white">{i + 1}</span>
<span className="absolute left-1/2 -translate-x-1/2 top-7 whitespace-nowrap text-[10px] bg-white/95 rounded px-1.5 py-0.5 shadow text-[#0F172A]">{m.label}</span>
<button onClick={() => remove(i)} className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-white text-red-500 shadow flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"><Trash2 className="w-2.5 h-2.5" /></button>
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-destructive text-white text-[11px] font-bold shadow ring-2 ring-white">{i + 1}</span>
<span className="absolute left-1/2 -translate-x-1/2 top-7 whitespace-nowrap text-[10px] bg-white/95 rounded px-1.5 py-0.5 shadow text-ink-1">{m.label}</span>
<button onClick={() => remove(i)} className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-white text-destructive shadow flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"><Trash2 className="w-2.5 h-2.5" /></button>
</div>
))}
{pending && (
<div className="absolute -translate-x-1/2 -translate-y-1/2 pointer-events-none" style={{ left: `${pending.x * 100}%`, top: `${pending.y * 100}%` }}>
<span className="block w-6 h-6 rounded-full bg-[#0A39DF] ring-4 ring-white animate-pulse" />
<span className="block w-6 h-6 rounded-full bg-krow-blue ring-4 ring-white animate-pulse" />
</div>
)}
</div>
{pending && (
<div className="rounded-xl border border-[#0A39DF] bg-[#EFF6FF] p-3 mb-3">
<p className="text-[12px] font-medium text-[#1E40AF] mb-2">Label this hazard:</p>
<div className="rounded-xl border border-krow-blue bg-krow-blue-tint p-3 mb-3">
<p className="text-[12px] font-medium text-krow-blue mb-2">Label this hazard:</p>
<div className="flex flex-wrap gap-1.5 mb-2">
{PRESET_TAGS.map((t) => (
<button key={t} onClick={() => confirm(t)} className="text-[11px] px-2.5 py-1 rounded-full bg-white border border-[#E5E7EB] text-[#374151] hover:border-[#0A39DF]">{t}</button>
<button key={t} onClick={() => confirm(t)} className="text-[11px] px-2.5 py-1 rounded-full bg-white border border-border text-ink-2 hover:border-krow-blue">{t}</button>
))}
</div>
<div className="flex gap-2">
<input value={customLabel} onChange={(e) => setCustomLabel(e.target.value)} placeholder="Or type your own…" className="flex-1 h-8 px-2.5 text-[12px] rounded-lg border border-[#E5E7EB] focus:border-[#0A39DF] focus:outline-none" />
<button onClick={() => customLabel.trim() && confirm(customLabel.trim())} className="h-8 px-3 rounded-lg bg-[#0A39DF] text-white text-[12px] font-medium flex items-center gap-1"><Plus className="w-3.5 h-3.5" /> Add</button>
<input value={customLabel} onChange={(e) => setCustomLabel(e.target.value)} placeholder="Or type your own…" className="flex-1 h-8 px-2.5 text-[12px] rounded-lg border border-border focus:border-krow-blue focus:outline-none" />
<button onClick={() => customLabel.trim() && confirm(customLabel.trim())} className="h-8 px-3 rounded-lg bg-krow-blue text-white text-[12px] font-medium flex items-center gap-1"><Plus className="w-3.5 h-3.5" /> Add</button>
</div>
</div>
)}
<button onClick={() => onSubmit(markers)} disabled={markers.length === 0 || submitting || !!pending} className="w-full h-10 rounded-xl bg-[#0F172A] text-white text-[13px] font-semibold disabled:opacity-40">
<button onClick={() => onSubmit(markers)} disabled={markers.length === 0 || submitting || !!pending} className="w-full h-10 rounded-xl bg-krow-navy text-white text-[13px] font-semibold disabled:opacity-40">
Submit {markers.length} hazard{markers.length !== 1 ? 's' : ''} for AI evaluation
</button>
</div>

View File

@@ -28,11 +28,11 @@ export default function MediaChallenge({ course, onSubmit, submitting }) {
return (
<div>
<p className="text-[13px] text-[#64748B] mb-3">
<p className="text-[13px] text-ink-3 mb-3">
{type === 'video' ? 'Record or upload a short video demonstrating the challenge.' : 'Take or upload a photo showing your work.'}
</p>
{mediaUrl ? (
<div className="mb-3 rounded-xl overflow-hidden border border-[#E5E7EB]">
<div className="mb-3 rounded-xl overflow-hidden border border-border">
{type === 'video' ? (
<video src={mediaUrl} controls className="w-full max-h-64 object-cover" />
) : (
@@ -40,13 +40,13 @@ export default function MediaChallenge({ course, onSubmit, submitting }) {
)}
</div>
) : (
<label className="flex flex-col items-center justify-center h-40 rounded-xl border-2 border-dashed border-[#CBD5E1] cursor-pointer hover:border-[#0A39DF] transition-colors">
<label className="flex flex-col items-center justify-center h-40 rounded-xl border-2 border-dashed border-border cursor-pointer hover:border-krow-blue transition-colors">
{uploading ? (
<Loader2 className="w-6 h-6 animate-spin text-[#0A39DF]" />
<Loader2 className="w-6 h-6 animate-spin text-krow-blue" />
) : (
<>
<Camera className="w-6 h-6 text-[#94A3B8] mb-2" />
<span className="text-[13px] text-[#64748B]">Tap to {type === 'video' ? 'record' : 'capture'}</span>
<Camera className="w-6 h-6 text-ink-4 mb-2" />
<span className="text-[13px] text-ink-3">Tap to {type === 'video' ? 'record' : 'capture'}</span>
</>
)}
<input type="file" accept={accept} capture={capture} onChange={handleFile} className="hidden" disabled={uploading || submitting} />
@@ -54,12 +54,12 @@ export default function MediaChallenge({ course, onSubmit, submitting }) {
)}
<div className="flex gap-2">
{mediaUrl && (
<button onClick={() => setMediaUrl('')} className="h-10 px-4 rounded-xl border border-[#E5E7EB] text-[13px] font-medium text-[#64748B]">Retake</button>
<button onClick={() => setMediaUrl('')} className="h-10 px-4 rounded-xl border border-border text-[13px] font-medium text-ink-3">Retake</button>
)}
<button
onClick={() => onSubmit(mediaUrl)}
disabled={!mediaUrl || uploading || submitting}
className="flex-1 h-10 rounded-xl bg-[#0F172A] text-white text-[13px] font-semibold disabled:opacity-40"
className="flex-1 h-10 rounded-xl bg-krow-navy text-white text-[13px] font-semibold disabled:opacity-40"
>
Submit proof for AI evaluation
</button>

View File

@@ -39,8 +39,8 @@ export default function RoleplayChallenge({ course, onSubmit, submitting }) {
<div className="flex items-center gap-2 mb-3">
<OwliverAvatar className="w-8 h-8" rounded="rounded-full" />
<div>
<p className="text-[13px] font-semibold text-[#0F172A]">Owliver</p>
<p className="text-[10px] text-[#94A3B8]">Your AI skills coach</p>
<p className="text-[13px] font-semibold text-ink-1">Owliver</p>
<p className="text-[10px] text-ink-4">Your AI skills coach</p>
</div>
</div>
<div className="space-y-3 mb-4 max-h-72 overflow-y-auto pr-1">
@@ -49,8 +49,8 @@ export default function RoleplayChallenge({ course, onSubmit, submitting }) {
return (
<div key={i} className={`flex items-end gap-2 ${isAI ? 'justify-start' : 'justify-end'}`}>
{isAI && <OwliverAvatar className="w-7 h-7 shrink-0" rounded="rounded-full" />}
<div className={`max-w-[80%] px-3.5 py-2.5 rounded-2xl text-[13px] leading-relaxed ${
m.role === 'user' ? 'bg-[#0A39DF] text-white rounded-br-sm' : 'bg-[#F1F5F9] text-[#0F172A] rounded-bl-sm'
<div className={`max-w-[80%] px-3.5 py-2.5 rounded-xl text-[13px] leading-relaxed ${
m.role === 'user' ? 'bg-krow-blue text-white rounded-br-sm' : 'bg-surface-sunken text-ink-1 rounded-bl-sm'
}`}>{m.content}</div>
</div>
);
@@ -64,16 +64,16 @@ export default function RoleplayChallenge({ course, onSubmit, submitting }) {
onKeyDown={(e) => e.key === 'Enter' && send()}
disabled={submitting || busy}
placeholder="Type your response…"
className="flex-1 h-9 bg-transparent px-1 text-[13px] focus:outline-none placeholder:text-[#9CA3AF]"
className="flex-1 h-9 bg-transparent px-1 text-[13px] focus:outline-none placeholder:text-ink-4"
/>
<button onClick={send} disabled={busy || submitting} className="w-9 h-9 rounded-full bg-[#0A39DF] text-white flex items-center justify-center disabled:opacity-50 shrink-0">
<button onClick={send} disabled={busy || submitting} className="w-9 h-9 rounded-full bg-krow-blue text-white flex items-center justify-center disabled:opacity-50 shrink-0">
{busy ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
</button>
</div>
<button
onClick={() => onSubmit(transcript)}
disabled={!hasResponded || submitting || busy}
className="mt-3 w-full h-10 rounded-xl bg-[#0F172A] text-white text-[13px] font-semibold disabled:opacity-40"
className="mt-3 w-full h-10 rounded-xl bg-krow-navy text-white text-[13px] font-semibold disabled:opacity-40"
>
Submit proof for AI evaluation
</button>

View File

@@ -72,8 +72,8 @@ export default function VideoRecorder({ onSubmit, submitting }) {
return (
<div>
{error && <p className="text-[12px] text-[#EF4444] mb-3">{error}</p>}
<div className="relative rounded-2xl overflow-hidden bg-black aspect-video mb-3">
{error && <p className="text-[12px] text-destructive mb-3">{error}</p>}
<div className="relative rounded-xl overflow-hidden bg-black aspect-video mb-3">
{recordedUrl ? (
<video src={recordedUrl} controls className="w-full h-full object-cover" />
) : (
@@ -81,19 +81,19 @@ export default function VideoRecorder({ onSubmit, submitting }) {
)}
{recording && (
<div className="absolute top-3 left-3 flex items-center gap-1.5 bg-black/60 rounded-full px-2.5 py-1">
<span className="w-2 h-2 rounded-full bg-red-500 animate-pulse" />
<span className="w-2 h-2 rounded-full bg-destructive animate-pulse" />
<span className="text-[12px] text-white font-mono">{seconds}s</span>
</div>
)}
</div>
{!recordedUrl ? (
<button onClick={recording ? stop : start} disabled={uploading || submitting} className={`w-full h-11 rounded-xl font-semibold text-[14px] flex items-center justify-center gap-2 text-white ${recording ? 'bg-red-500' : 'bg-[#0A39DF]'}`}>
<button onClick={recording ? stop : start} disabled={uploading || submitting} className={`w-full h-11 rounded-xl font-semibold text-[14px] flex items-center justify-center gap-2 text-white ${recording ? 'bg-destructive' : 'bg-krow-blue'}`}>
{recording ? <><Square className="w-4 h-4" /> Stop recording</> : <><Video className="w-4 h-4" /> Record yourself</>}
</button>
) : (
<div className="flex gap-2">
<button onClick={reset} className="h-11 px-4 rounded-xl border border-[#E5E7EB] text-[13px] font-medium text-[#64748B] flex items-center gap-1.5"><RotateCcw className="w-4 h-4" /> Retake</button>
<button onClick={submit} disabled={uploading || submitting} className="flex-1 h-11 rounded-xl bg-[#0F172A] text-white text-[13px] font-semibold flex items-center justify-center gap-2 disabled:opacity-50">
<button onClick={reset} className="h-11 px-4 rounded-xl border border-border text-[13px] font-medium text-ink-3 flex items-center gap-1.5"><RotateCcw className="w-4 h-4" /> Retake</button>
<button onClick={submit} disabled={uploading || submitting} className="flex-1 h-11 rounded-xl bg-krow-navy text-white text-[13px] font-semibold flex items-center justify-center gap-2 disabled:opacity-50">
{uploading ? <Loader2 className="w-4 h-4 animate-spin" /> : null} Submit proof for AI evaluation
</button>
</div>