Update Owliver language and suggestions
This commit is contained in:
@@ -9,6 +9,7 @@ import { SkillRequirementsField } from '@/components/krow/SkillRequirementsField
|
||||
import { SkillSurface } from '@/components/skills/SkillSurface';
|
||||
import OwliverAvatar from '@/components/krow/OwliverAvatar';
|
||||
import { usePublishPageActions, usePublishPageContext } from '@/components/ai-assistant';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* Create a Position.
|
||||
@@ -29,6 +30,7 @@ interface CreatePositionProps {
|
||||
}
|
||||
|
||||
export default function CreatePosition({ prefill: prefillProp, embedded = false, onDone }: CreatePositionProps) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
/* Owliver opens this page with the fields it could infer from the request.
|
||||
It is a starting point, not a submission: every field stays editable and
|
||||
@@ -197,13 +199,13 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
{!embedded && (
|
||||
<div className="flex items-start justify-between gap-4 mb-6">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-[22px] font-bold text-[#111827]">Create a Position</h1>
|
||||
<p className="text-[13px] text-[#6B7280] mt-0.5">Set hiring criteria once — KROW interviews and ranks every applicant automatically.</p>
|
||||
<h1 className="text-[22px] font-bold text-[#111827]">{t('createPosition.createAPosition')}</h1>
|
||||
<p className="text-[13px] text-[#6B7280] mt-0.5">{t('createPosition.setHiringCriteriaOnceKrowInterviews')}</p>
|
||||
</div>
|
||||
{/* `shrink-0` and a real gap: without them the subtitle's intrinsic
|
||||
width pushed Cancel against the edge of a phone screen with the
|
||||
title wrapping underneath it. */}
|
||||
<button onClick={() => navigate('/admin/positions')} className="shrink-0 pt-1 text-[13px] text-[#6B7280] hover:text-[#111827]">Cancel</button>
|
||||
<button onClick={() => navigate('/admin/positions')} className="shrink-0 pt-1 text-[13px] text-[#6B7280] hover:text-[#111827]">{t('createPosition.cancel')}</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -214,7 +216,7 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
{/* Section Header */}
|
||||
<div className="flex items-center gap-2">
|
||||
<OwliverAvatar className="w-7 h-7" rounded="rounded-lg" />
|
||||
<h2 className="text-[16px] font-semibold text-[#111827]">Create Job Posting</h2>
|
||||
<h2 className="text-[16px] font-semibold text-[#111827]">{t('createPosition.createJobPosting')}</h2>
|
||||
</div>
|
||||
|
||||
{/* Extension points. A definition naming `create-position` and one of
|
||||
@@ -226,14 +228,14 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
{/* Row 1 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-[13px] font-medium text-[#374151]">Role Category *</label>
|
||||
<label className="text-[13px] font-medium text-[#374151]">{t('createPosition.roleCategory')}</label>
|
||||
<select value={form.role_category} onChange={(e) => update('role_category', e.target.value)} className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none">
|
||||
{allCategories.map(r => <option key={r} value={r}>{r}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[13px] font-medium text-[#374151]">Job Title *</label>
|
||||
<input value={form.title} onChange={(e) => update('title', e.target.value)} placeholder="e.g. Experienced Server" className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
<label className="text-[13px] font-medium text-[#374151]">{t('createPosition.jobTitle')}</label>
|
||||
<input value={form.title} onChange={(e) => update('title', e.target.value)} placeholder={t('createPosition.eGExperiencedServer')} className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -242,18 +244,18 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
hire is actually for — the field existed in the model and had no way
|
||||
in from this form until now. */}
|
||||
<div>
|
||||
<label className="text-[13px] font-medium text-[#374151]">Client / Company</label>
|
||||
<label className="text-[13px] font-medium text-[#374151]">{t('createPosition.clientCompany')}</label>
|
||||
<input
|
||||
value={form.company}
|
||||
onChange={(e) => update('company', e.target.value)}
|
||||
placeholder="e.g. Fairmont San Jose"
|
||||
placeholder={t('createPosition.eGFairmontSanJose')}
|
||||
className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Add new category */}
|
||||
<div className="flex gap-2">
|
||||
<input value={newCategory} onChange={(e) => setNewCategory(e.target.value)} placeholder="Create a new role category (saved for reuse)..." className="flex-1 h-10 rounded-lg border border-[#E5E7EB] px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
<input value={newCategory} onChange={(e) => setNewCategory(e.target.value)} placeholder={t('createPosition.createANewRoleCategorySaved')} className="flex-1 h-10 rounded-lg border border-[#E5E7EB] px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (newCategory.trim()) {
|
||||
@@ -267,14 +269,14 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
disabled={createRoleCategory.isPending || !newCategory.trim()}
|
||||
className="px-4 bg-[#0838E0] text-white text-[13px] font-medium rounded-lg hover:bg-[#062BAF] disabled:opacity-50 whitespace-nowrap"
|
||||
>
|
||||
+ Add
|
||||
{t('createPosition.add')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Requirements */}
|
||||
<div>
|
||||
<label className="text-[13px] font-medium text-[#374151]">Custom Requirements (AI will use this)</label>
|
||||
<textarea value={form.custom_requirements} onChange={(e) => update('custom_requirements', e.target.value)} placeholder="e.g. Must have fine dining experience..." className="w-full mt-1.5 min-h-[70px] p-3 text-[13px] rounded-lg border border-[#E5E7EB] focus:border-[#0838E0] focus:outline-none resize-none" />
|
||||
<label className="text-[13px] font-medium text-[#374151]">{t('createPosition.customRequirementsAiWillUseThis')}</label>
|
||||
<textarea value={form.custom_requirements} onChange={(e) => update('custom_requirements', e.target.value)} placeholder={t('createPosition.eGMustHaveFineDining')} className="w-full mt-1.5 min-h-[70px] p-3 text-[13px] rounded-lg border border-[#E5E7EB] focus:border-[#0838E0] focus:outline-none resize-none" />
|
||||
</div>
|
||||
|
||||
{/* Physical / Leadership / Attendance Expectations.
|
||||
@@ -285,60 +287,60 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="text-[13px] font-medium text-[#374151]">
|
||||
Physical Requirements <span className="font-normal text-[#9CA3AF]">— optional</span>
|
||||
{t('createPosition.physicalRequirements')} <span className="font-normal text-[#9CA3AF]">{t('createPosition.optional')}</span>
|
||||
</label>
|
||||
<p className="mt-0.5 text-[11px] text-[#9CA3AF]">Only if this role genuinely has them.</p>
|
||||
<input value={form.physical_requirements} onChange={(e) => update('physical_requirements', e.target.value)} placeholder="e.g. Extended standing, lifting or climbing" className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
<p className="mt-0.5 text-[11px] text-[#9CA3AF]">{t('createPosition.onlyIfThisRoleGenuinelyHas')}</p>
|
||||
<input value={form.physical_requirements} onChange={(e) => update('physical_requirements', e.target.value)} placeholder={t('createPosition.eGExtendedStandingLiftingOr')} className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[13px] font-medium text-[#374151]">
|
||||
Leadership Expectations <span className="font-normal text-[#9CA3AF]">— optional</span>
|
||||
{t('createPosition.leadershipExpectations')} <span className="font-normal text-[#9CA3AF]">{t('createPosition.optional')}</span>
|
||||
</label>
|
||||
<p className="mt-0.5 text-[11px] text-[#9CA3AF]">Only if this role leads people.</p>
|
||||
<input value={form.leadership_expectations} onChange={(e) => update('leadership_expectations', e.target.value)} placeholder="e.g. Lead a team of 5" className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
<p className="mt-0.5 text-[11px] text-[#9CA3AF]">{t('createPosition.onlyIfThisRoleLeadsPeople')}</p>
|
||||
<input value={form.leadership_expectations} onChange={(e) => update('leadership_expectations', e.target.value)} placeholder={t('createPosition.eGLeadATeamOf')} className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[13px] font-medium text-[#374151]">
|
||||
Attendance Expectations <span className="font-normal text-[#9CA3AF]">— optional</span>
|
||||
{t('createPosition.attendanceExpectations')} <span className="font-normal text-[#9CA3AF]">{t('createPosition.optional')}</span>
|
||||
</label>
|
||||
<p className="mt-0.5 text-[11px] text-[#9CA3AF]">Shift pattern or availability, if it matters.</p>
|
||||
<input value={form.attendance_expectations} onChange={(e) => update('attendance_expectations', e.target.value)} placeholder="e.g. On-time, no weekends off" className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
<p className="mt-0.5 text-[11px] text-[#9CA3AF]">{t('createPosition.shiftPatternOrAvailabilityIfIt')}</p>
|
||||
<input value={form.attendance_expectations} onChange={(e) => update('attendance_expectations', e.target.value)} placeholder={t('createPosition.eGOnTimeNoWeekends')} className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="text-[13px] font-medium text-[#374151]">Min. Experience (yrs)</label>
|
||||
<label className="text-[13px] font-medium text-[#374151]">{t('createPosition.minExperienceYrs')}</label>
|
||||
<input type="number" min="0" value={form.min_experience_years} onChange={(e) => update('min_experience_years', parseInt(e.target.value) || 0)} className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[13px] font-medium text-[#374151]">English Level</label>
|
||||
<label className="text-[13px] font-medium text-[#374151]">{t('createPosition.englishLevel')}</label>
|
||||
<select value={form.english_required} onChange={(e) => update('english_required', e.target.value)} className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none">
|
||||
{ENGLISH_LEVELS.map(l => <option key={l.value} value={l.value}>{l.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[13px] font-medium text-[#374151]">Location</label>
|
||||
<input value={form.location} onChange={(e) => update('location', e.target.value)} placeholder="Bay Area, LA..." className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
<label className="text-[13px] font-medium text-[#374151]">{t('createPosition.location')}</label>
|
||||
<input value={form.location} onChange={(e) => update('location', e.target.value)} placeholder={t('createPosition.bayAreaLa')} className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-[13px] font-medium text-[#374151]">Pay Min ($/hr)</label>
|
||||
<label className="text-[13px] font-medium text-[#374151]">{t('createPosition.payMinHr')}</label>
|
||||
<input type="number" min="0" value={form.pay_range_min} onChange={(e) => update('pay_range_min', e.target.value)} placeholder="e.g. 18" className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[13px] font-medium text-[#374151]">Pay Max ($/hr)</label>
|
||||
<label className="text-[13px] font-medium text-[#374151]">{t('createPosition.payMaxHr')}</label>
|
||||
<input type="number" min="0" value={form.pay_range_max} onChange={(e) => update('pay_range_max', e.target.value)} placeholder="e.g. 28" className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Certifications */}
|
||||
<div>
|
||||
<label className="text-[13px] font-medium text-[#374151]">Required Certifications</label>
|
||||
<label className="text-[13px] font-medium text-[#374151]">{t('createPosition.requiredCertifications')}</label>
|
||||
<div className="flex flex-wrap gap-2 mt-1.5">
|
||||
{CERT_OPTIONS.map(cert => (
|
||||
<button
|
||||
@@ -370,9 +372,9 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
className="w-full bg-[#0838E0] hover:bg-[#062BAF] disabled:opacity-60 text-white font-medium py-3 rounded-xl text-[14px] flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
{generateDesc.isPending || createJob.isPending ? (
|
||||
<><Loader2 className="w-4 h-4 animate-spin" /> Generating...</>
|
||||
<><Loader2 className="w-4 h-4 animate-spin" /> {t('createPosition.generating')}</>
|
||||
) : (
|
||||
<><Star className="w-4 h-4" /> Generate Job Description with AI</>
|
||||
<><Star className="w-4 h-4" /> {t('createPosition.generateJobDescriptionWithAi')}</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -380,12 +382,12 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
{aiResult && (
|
||||
<div className="rounded-xl border border-[#DBEAFE] bg-[#EEF3FE] p-4 space-y-3">
|
||||
<div className="flex items-center gap-1.5 text-[#1E40AF] text-[13px] font-medium">
|
||||
<Sparkles className="w-3.5 h-3.5" /> AI-Generated Description
|
||||
<Sparkles className="w-3.5 h-3.5" /> {t('createPosition.aiGeneratedDescription')}
|
||||
</div>
|
||||
<p className="text-[13px] text-[#374151] whitespace-pre-wrap">{aiResult.description}</p>
|
||||
{aiResult.responsibilities?.length > 0 && (
|
||||
<div>
|
||||
<p className="text-[12px] font-semibold text-[#374151] mb-1">Key Responsibilities</p>
|
||||
<p className="text-[12px] font-semibold text-[#374151] mb-1">{t('createPosition.keyResponsibilities')}</p>
|
||||
<ul className="text-[12px] text-[#6B7280] list-disc list-inside space-y-0.5">
|
||||
{aiResult.responsibilities.map((r, i) => <li key={i}>{r}</li>)}
|
||||
</ul>
|
||||
@@ -405,7 +407,7 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
<button onClick={() => setShowWeights(!showWeights)} className="w-full flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Sliders className="w-4 h-4 text-[#059669]" />
|
||||
<span className="text-[14px] font-medium text-[#059669]">AI Vetting Weights (Total: {totalWeight}% — must = 100%)</span>
|
||||
<span className="text-[14px] font-medium text-[#059669]">{t('createPosition.aiVettingWeightsTotal')} {totalWeight}{t('createPosition.must100')}</span>
|
||||
</span>
|
||||
{showWeights ? <ChevronUp className="w-4 h-4 text-[#9CA3AF]" /> : <ChevronDown className="w-4 h-4 text-[#9CA3AF]" />}
|
||||
</button>
|
||||
@@ -421,7 +423,7 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
</div>
|
||||
))}
|
||||
<div className="md:col-span-2 text-[12px] text-[#6B7280]">
|
||||
Total: <span className={totalWeight === 100 ? 'text-[#059669] font-medium' : 'text-[#F59E0B] font-medium'}>{totalWeight}%</span>
|
||||
{t('createPosition.total')} <span className={totalWeight === 100 ? 'text-[#059669] font-medium' : 'text-[#F59E0B] font-medium'}>{totalWeight}%</span>
|
||||
{totalWeight !== 100 && ' — adjust to reach 100%'}
|
||||
</div>
|
||||
</div>
|
||||
@@ -443,10 +445,10 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
{/* Bottom Actions */}
|
||||
<div className="grid grid-cols-2 gap-3 pt-2">
|
||||
<button onClick={() => handleSave('draft')} disabled={createJob.isPending || updateJob.isPending} className="border border-[#E5E7EB] hover:bg-[#F9FAFB] text-[#374151] font-medium py-3 rounded-xl text-[14px] transition-colors disabled:opacity-50">
|
||||
Save as Draft
|
||||
{t('createPosition.saveAsDraft')}
|
||||
</button>
|
||||
<button onClick={() => handleSave('active')} disabled={createJob.isPending || updateJob.isPending} className="bg-[#0838E0] hover:bg-[#062BAF] text-white font-medium py-3 rounded-xl text-[14px] transition-colors disabled:opacity-50">
|
||||
Publish Job Posting
|
||||
{t('createPosition.publishJobPosting')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user