fix Owliver ai chat

This commit is contained in:
2026-08-10 13:19:10 +05:30
parent 52fe6d9c14
commit 76412b08b4
8 changed files with 57 additions and 272 deletions

View File

@@ -5,7 +5,7 @@ import { Surface } from '@/components/ds/Surface';
import { IconButton } from '@/components/ds/IconButton'; import { IconButton } from '@/components/ds/IconButton';
import { Alert } from '@/components/ds/Alert'; import { Alert } from '@/components/ds/Alert';
import { KROW_LOGO_URL } from '@/assets/brand'; import { KROW_LOGO_URL } from '@/assets/brand';
import { useAssistantFacts, useConversation } from './useAssistant'; import { useAssistantFacts, useConversation, useCurrentUserName } from './useAssistant';
import { buildIntro, buildPrompts } from './dynamic'; import { buildIntro, buildPrompts } from './dynamic';
import { Message, ThinkingIndicator, TurnDivider } from './AssistantMessage'; import { Message, ThinkingIndicator, TurnDivider } from './AssistantMessage';
import { PromptInput } from './PromptInput'; import { PromptInput } from './PromptInput';
@@ -43,7 +43,8 @@ export default function KrowAssistant({
onExpand, onExpand,
onRestore, onRestore,
}) { }) {
const facts = useAssistantFacts({ needsWorkerProfile: context.needsWorkerProfile }); const facts = useAssistantFacts();
const userName = useCurrentUserName();
const { messages, pending, error, busy, send, stop, reset } = useConversation({ const { messages, pending, error, busy, send, stop, reset } = useConversation({
contextId: context.id, contextId: context.id,
facts, facts,
@@ -56,8 +57,8 @@ export default function KrowAssistant({
/* Greeting and suggestions come from live data, so they recompute only when /* Greeting and suggestions come from live data, so they recompute only when
the data or the page actually changes. */ the data or the page actually changes. */
const intro = React.useMemo( const intro = React.useMemo(
() => buildIntro(context.id, facts, facts.profile?.full_name), () => buildIntro(context.id, facts, userName),
[context.id, facts] [context.id, facts, userName]
); );
const prompts = React.useMemo(() => buildPrompts(context.id, facts), [context.id, facts]); const prompts = React.useMemo(() => buildPrompts(context.id, facts), [context.id, facts]);

View File

@@ -1,204 +0,0 @@
import { BookOpen, Compass, FileText } from 'lucide-react';
import { actions, answer, badges, doc, heading, insights, kpis, meters, note, status, table, text } from '../blocks';
import { plural } from '../insights';
import { getScoreBand, toFICO } from '@/lib/talentHome';
/**
* Talent capabilities — My Portal.
*
* The register shifts here: an employer wants a read on a pipeline, a worker
* wants to know what to do next to earn more. Answers name the specific course
* or step, because "improve your skills" helps nobody.
*/
const scoreOf = (p) => Number(p?.krow_score) || 0;
const NO_PROFILE = 'I do not have your profile yet. Once it exists I can give you specifics instead of generalities.';
const resumeReview = (f) => {
const p = f.profile;
if (!p) return answer(NO_PROFILE);
const fields = [
{ label: 'Photo', ok: Boolean(p.selfie_url), note: 'The KROW ID replaces a résumé — a profile with a face gets opened more.' },
{ label: 'Availability', ok: (p.availability || []).length > 0, note: 'Employers filter on this before they read anything else.' },
{ label: 'Certifications', ok: (p.certifications || []).length > 0, note: 'A Food Handler Card is the cheapest credential in this industry.' },
{ label: 'Skills', ok: (p.skills || []).length > 0, note: 'Without these you do not appear in skill searches.' },
{ label: 'Work history', ok: (p.experience || []).length > 0, note: 'Even one role gives employers something to anchor on.' },
{ label: 'Career goals', ok: Boolean(p.career_goals), note: 'Lets matching put you in front of the right roles, not every role.' },
{ label: 'Verified evidence', ok: (p.capabilities || []).length > 0, note: 'The only part of your profile an employer does not have to take on trust.' },
];
const complete = fields.filter((x) => x.ok).length;
return doc(
heading(`Your profile is ${p.profile_completion || 0}% complete`, `${complete} of ${fields.length} things employers look for`),
status(fields.map((x) => ({ label: x.label, ok: x.ok, note: x.ok ? undefined : x.note }))),
(p.certifications || []).length && [
heading('Credentials on file'),
badges(p.certifications.map((label) => ({ label, tone: 'success' }))),
],
text(
complete === fields.length
? 'Nothing is missing. From here, more verified evidence is what moves you — not more fields.'
: 'If you fix two things, fix availability and certifications. Those are the two employers filter on first.'
),
note(
(p.capabilities || []).length
? 'Your verified capabilities came with evidence attached, not a checkbox. That is why they carry weight.'
: 'Nothing is verified yet. One completed challenge is worth more than ten claimed skills.'
)
);
};
const careerGuidance = (f) => {
const p = f.profile;
if (!p) return answer(NO_PROFILE);
const score = scoreOf(p);
const band = getScoreBand(score);
const target = score >= 90 ? null : score >= 75 ? 90 : score >= 60 ? 75 : 60;
return doc(
heading(`${toFICO(score)} — ${band.label}`, 'Career score, on a 300–850 scale'),
kpis([
{ label: 'Reliability', value: p.reliability_score || 0, tone: (p.reliability_score || 0) >= 70 ? 'success' : 'warning' },
{ label: 'Attendance', value: p.attendance_score ?? 0, tone: (p.attendance_score ?? 0) >= 90 ? 'success' : undefined },
{ label: 'Shifts worked', value: p.shifts_completed || 0, tone: (p.shifts_completed || 0) === 0 ? 'warning' : undefined },
{ label: 'Verified skills', value: (p.capabilities || []).length },
]),
heading("What's driving it"),
meters([
{ label: 'Reliability', value: p.reliability_score || 0, weight: 25 },
{ label: 'Attendance', value: p.attendance_score ?? 0, weight: 20 },
{ label: 'Communication', value: p.performance_score || 0, weight: 15 },
{ label: 'Leadership', value: p.leadership_potential || 0, weight: 15 },
{ label: 'Learning', value: Math.min(100, (p.completed_courses || []).length * 8 + (p.xp || 0) / 10), weight: 15 },
{ label: 'Problem solving', value: p.ai_interview_score || 0, weight: 10 },
]),
insights([
target
? {
tone: 'info',
title: `To reach ${target}`,
body: 'Verified work is the fastest lever. Courses move the score; verified shifts move it more, because employers weight what someone else confirmed over what you claimed.',
}
: {
tone: 'success',
title: 'You are in the top band',
body: 'From here it is about holding it — the score decays if shifts stop.',
},
(p.shifts_completed || 0) === 0 && {
tone: 'warning',
title: 'Work one shift',
body: 'Attendance, ratings and reliability all need a first data point before anything else compounds.',
},
p.desired_position
? {
tone: score >= 75 ? 'success' : 'info',
title: `Aiming for ${p.desired_position}`,
body: score >= 75
? 'You are already scoring in the range employers shortlist for that.'
: 'The gap to that role is verification, not ambition.',
}
: {
tone: 'info',
title: 'No target role set',
body: 'Setting one lets me tell you which courses matter and which are noise.',
},
])
);
};
const learningRecommendations = (f) => {
const p = f.profile;
if (!p) return answer(NO_PROFILE);
const completedIds = new Set((p.completed_courses || []).map((c) => c.course_id));
const done = (p.completed_courses || []).length;
const shifts = p.shifts_completed || 0;
const available = (f.courses || []).filter((c) => c.status === 'active' && !completedIds.has(c.id));
if (!available.length) {
return doc(
heading('You have completed everything available'),
kpis([
{ label: 'Courses', value: done, tone: 'success' },
{ label: 'Badges', value: (p.earned_badges || []).length, tone: 'success' },
{ label: 'XP', value: p.xp || 0 },
{ label: 'Verified', value: (p.capabilities || []).length },
]),
text('The next lever is verified shifts — that is what employers weight most.')
);
}
const ranked = [...available].sort((a, b) => (b.xp || 0) - (a.xp || 0));
return doc(
heading('What to learn next', 'Ranked by what moves your score most'),
table(
[
{ key: 'title', label: 'Course' },
{ key: 'minutes', label: 'Time', align: 'right' },
{ key: 'xp', label: 'XP', align: 'right' },
],
ranked.slice(0, 4).map((c) => {
const gate = c.unlock_requirements?.min_shifts || 0;
const locked = gate > shifts;
return {
title: c.title,
minutes: `${c.estimated_minutes}m`,
xp: locked
? { badge: `${gate} shifts`, tone: 'warning' }
: { value: `+${c.xp}`, tone: 'success' },
};
})
),
heading('Where you stand'),
kpis([
{ label: 'Courses done', value: done },
{ label: 'Badges', value: (p.earned_badges || []).length },
{ label: 'XP', value: p.xp || 0 },
{ label: 'Verified skills', value: (p.capabilities || []).length },
]),
actions([
{
title: done === 0 ? `Start with ${ranked[ranked.length - 1].title}` : `Take ${ranked[0].title}`,
body: done === 0
? 'The point of the first course is not the XP — it is getting one verified capability on the board.'
: done < 3
? `Three completions is where the score starts moving noticeably. You are at ${done}.`
: 'You are past the easy gains. Advanced courses are worth double the XP and unlock supervisor-tier roles.',
},
shifts < 5 && {
title: `Log ${plural(5 - shifts, 'more shift')}`,
body: `Advanced challenges stay locked until 5 shifts — you have ${shifts}. That gate is deliberate: leadership evidence from someone who has never run a floor is not evidence.`,
},
]),
note('Each course ends in a challenge, not a quiz — you demonstrate the skill and the evidence gets scored.')
);
};
/* ── Free-text responder ────────────────────────────────────────────────── */
const has = (q, ...words) => words.some((w) => q.includes(w));
export const respondTalent = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'resume', 'résumé', 'cv', 'profile', 'improve')) return resumeReview(f);
if (has(q, 'learn', 'course', 'training', 'study', 'skill', 'badge', 'path')) return learningRecommendations(f);
if (has(q, 'career', 'earn more', 'pay', 'score', 'guidance', 'promotion', 'next', 'started')) return careerGuidance(f);
const p = f.profile;
return answer(
p
? `I can help with your career score (currently ${toFICO(scoreOf(p))}), your profile, or what to learn next. Where would you like to start?`
: NO_PROFILE
);
};
export const TALENT_CAPABILITIES = [
{ id: 'resume-review', label: 'Resume Review', icon: FileText, run: resumeReview },
{ id: 'career-guidance', label: 'Career Guidance', icon: Compass, run: careerGuidance },
{ id: 'learning-recommendations', label: 'Learning Recommendations', icon: BookOpen, run: learningRecommendations },
];

View File

@@ -6,7 +6,6 @@ import {
ACTIVITY_CAPABILITIES, ADMIN_CANDIDATE_CAPABILITIES, CONTROL_CENTER_CAPABILITIES, ACTIVITY_CAPABILITIES, ADMIN_CANDIDATE_CAPABILITIES, CONTROL_CENTER_CAPABILITIES,
respondActivity, respondAdminCandidates, respondControlCenter, respondActivity, respondAdminCandidates, respondControlCenter,
} from './capabilities/admin'; } from './capabilities/admin';
import { TALENT_CAPABILITIES, respondTalent } from './capabilities/talent';
/** /**
* Page contexts for the Krow AI Assistant. * Page contexts for the Krow AI Assistant.
@@ -16,8 +15,8 @@ import { TALENT_CAPABILITIES, respondTalent } from './capabilities/talent';
* live data in `dynamic.js`, not written here. A context only declares what this * live data in `dynamic.js`, not written here. A context only declares what this
* page's assistant can *do*: its capabilities and its free-text responder. * page's assistant can *do*: its capabilities and its free-text responder.
* *
* `needsWorkerProfile` gates the one query with a side effect, so employer and * Krow AI serves Employer and Admin. The talent portal has Owliver, a separate
* admin sessions never touch worker-profile records. * product with its own UI and workflow — nothing here targets it.
*/ */
export const ASSISTANT_CONTEXTS = { export const ASSISTANT_CONTEXTS = {
'employer.overview': { 'employer.overview': {
@@ -56,13 +55,6 @@ export const ASSISTANT_CONTEXTS = {
capabilities: ACTIVITY_CAPABILITIES, capabilities: ACTIVITY_CAPABILITIES,
respond: respondActivity, respond: respondActivity,
}, },
'talent.portal': {
id: 'talent.portal',
page: 'My Portal',
capabilities: TALENT_CAPABILITIES,
respond: respondTalent,
needsWorkerProfile: true,
},
}; };
export const getContext = (id) => ASSISTANT_CONTEXTS[id] ?? null; export const getContext = (id) => ASSISTANT_CONTEXTS[id] ?? null;

View File

@@ -98,14 +98,6 @@ const GREETINGS = {
return `${plural(f.activity24h.length, 'event')} today, ${f.activity7d.length} this week.`; return `${plural(f.activity24h.length, 'event')} today, ${f.activity7d.length} this week.`;
}, },
'talent.portal': (f) => {
const p = f.profile;
if (!p) return 'Let us get your profile started.';
const completion = p.profile_completion || 0;
if (completion >= 90) return 'Your profile is nearly complete — nice work.';
if (completion > 0) return `You are ${completion}% of the way through your profile.`;
return 'Your profile is empty. A few fields will change what employers see.';
},
}; };
/** Title lines. First names read better than a formal greeting in a small panel. */ /** Title lines. First names read better than a formal greeting in a small panel. */
@@ -116,7 +108,6 @@ const TITLES = {
'admin.controlCenter': (f) => `Platform check, ${timeOfDay(f.today)}`, 'admin.controlCenter': (f) => `Platform check, ${timeOfDay(f.today)}`,
'admin.candidates': () => 'Talent analysis', 'admin.candidates': () => 'Talent analysis',
'admin.activity': () => 'Activity review', 'admin.activity': () => 'Activity review',
'talent.portal': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`,
}; };
/** Descriptions state what this page's assistant can actually do with the data. */ /** Descriptions state what this page's assistant can actually do with the data. */
@@ -128,7 +119,6 @@ const DESCRIPTIONS = {
'admin.controlCenter': () => 'Health checks, data integrity and what needs intervention.', 'admin.controlCenter': () => 'Health checks, data integrity and what needs intervention.',
'admin.candidates': () => 'Talent supply and risk across the candidate pool.', 'admin.candidates': () => 'Talent supply and risk across the candidate pool.',
'admin.activity': (f) => `${plural(f.activity.length, 'logged event')}, reconciled and profiled.`, 'admin.activity': (f) => `${plural(f.activity.length, 'logged event')}, reconciled and profiled.`,
'talent.portal': () => 'Your score, your profile, and what to learn next.',
}; };
/** Placeholders phrased as things this page's user would actually type. */ /** Placeholders phrased as things this page's user would actually type. */
@@ -151,7 +141,6 @@ const PLACEHOLDERS = {
'admin.controlCenter': () => ['Show unusual activity…', 'Summarize platform usage…'], 'admin.controlCenter': () => ['Show unusual activity…', 'Summarize platform usage…'],
'admin.candidates': () => ['Where is the risk in the pool?', 'Summarize recruitment trends…'], 'admin.candidates': () => ['Where is the risk in the pool?', 'Summarize recruitment trends…'],
'admin.activity': () => ['Show unusual activity…', 'Summarize platform usage…'], 'admin.activity': () => ['Show unusual activity…', 'Summarize platform usage…'],
'talent.portal': () => ['Improve my profile…', 'Recommend learning paths…'],
}; };
/* ── Suggested prompts ──────────────────────────────────────────────────── */ /* ── Suggested prompts ──────────────────────────────────────────────────── */
@@ -229,16 +218,6 @@ const PROMPTS = {
: null, : null,
].filter(Boolean), ].filter(Boolean),
'talent.portal': (f) => {
const p = f.profile;
return [
{ label: 'Improve my profile', prompt: 'Review my profile and tell me what is missing', capability: 'resume-review' },
{ label: 'Recommend learning paths', prompt: 'What should I learn next?', capability: 'learning-recommendations' },
p && (p.krow_score || 0) > 0
? { label: 'How do I score higher?', prompt: 'How do I raise my career score?', capability: 'career-guidance' }
: { label: 'How do I get started?', prompt: 'How do I raise my career score?', capability: 'career-guidance' },
];
},
}; };
/* ── Public API ─────────────────────────────────────────────────────────── */ /* ── Public API ─────────────────────────────────────────────────────────── */

View File

@@ -22,9 +22,6 @@ const PLACEMENT = {
'/candidates': 'admin.candidates', // Candidates Analysis '/candidates': 'admin.candidates', // Candidates Analysis
'/tracking': 'admin.activity', // Activity '/tracking': 'admin.activity', // Activity
}, },
talent: {
'/employee': 'talent.portal', // My Portal
},
}; };
/** /**
@@ -34,13 +31,16 @@ const PLACEMENT = {
* Employer: Positions, Position Detail, Create Position, Hired History, * Employer: Positions, Position Detail, Create Position, Hired History,
* Talent Pool, Profile. * Talent Pool, Profile.
* Admin: everything except the three above. * Admin: everything except the three above.
* Talent: everything except My Portal. * Talent: every page. The talent portal has Owliver — a separate product with
* its own voice experience, branding and workflow. Krow AI does not
* appear there and must not replace it.
*/ */
export const EXCLUDED_ROUTES = [ export const EXCLUDED_ROUTES = [
'/positions', '/positions/new', '/positions/:id', '/positions', '/positions/new', '/positions/:id',
'/hired', '/talent-pool', '/profile', '/apply', '/hired', '/talent-pool', '/profile', '/apply',
'/university', '/university/:id', '/me', '/identity', '/university', '/university/:id', '/me', '/identity',
'/owliver', '/design-system', '/owliver', '/design-system',
'/employee', // Talent portal — Owliver's, not Krow AI's.
]; ];
/** Returns the context for a role and path, or `null` to render no assistant. */ /** Returns the context for a role and path, or `null` to render no assistant. */

View File

@@ -1,7 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import { useQuery } from '@tanstack/react-query';
import { base44 } from '@/api/base44Client';
import { import {
useApplications, useCourses, useInterviews, useJobPostings, useStaff, useApplications, useCourses, useInterviews, useJobPostings, useStaff,
useUserActivity, useWorkerProfile, useWorkerProfiles, useUserActivity, useWorkerProfiles,
} from '@/lib/krowHooks'; } from '@/lib/krowHooks';
import { buildFacts } from './insights'; import { buildFacts } from './insights';
import { createAssistantProvider } from './provider'; import { createAssistantProvider } from './provider';
@@ -14,7 +16,7 @@ const provider = createAssistantProvider();
* costs no extra requests and cannot be looking at a different snapshot than the * costs no extra requests and cannot be looking at a different snapshot than the
* card beside it. * card beside it.
*/ */
export function useAssistantFacts({ needsWorkerProfile = false } = {}) { export function useAssistantFacts() {
const { data: applications = [] } = useApplications(); const { data: applications = [] } = useApplications();
const { data: postings = [] } = useJobPostings(); const { data: postings = [] } = useJobPostings();
const { data: interviews = [] } = useInterviews(); const { data: interviews = [] } = useInterviews();
@@ -23,19 +25,30 @@ export function useAssistantFacts({ needsWorkerProfile = false } = {}) {
const { data: activity = [] } = useUserActivity(); const { data: activity = [] } = useUserActivity();
const { data: courses = [] } = useCourses(); const { data: courses = [] } = useCourses();
// Gated: this query creates the record if absent, so it must not run for // Krow AI serves Employer and Admin only, so it never reads the signed-in
// employer or admin sessions. // worker's own profile — that query also creates the record if absent, and it
const { data: profile = null } = useWorkerProfile({ enabled: needsWorkerProfile }); // belongs to the talent portal, not here.
return React.useMemo( return React.useMemo(
() => buildFacts({ () => buildFacts({ applications, postings, interviews, staff, profiles, activity, courses }),
applications, postings, interviews, staff, profiles, activity, courses, [applications, postings, interviews, staff, profiles, activity, courses]
profile: needsWorkerProfile ? profile : null,
}),
[applications, postings, interviews, staff, profiles, activity, courses, profile, needsWorkerProfile]
); );
} }
/**
* The signed-in user's first name, for personalizing the greeting.
*
* Shares the `['user']` query key with the rest of the app, so this is a cache
* read rather than another request.
*/
export function useCurrentUserName() {
const { data } = useQuery({
queryKey: ['user'],
queryFn: () => base44.auth.me().catch(() => null),
staleTime: Infinity,
});
return data?.full_name || '';
}
/** /**
* Conversation state for one page context. * Conversation state for one page context.
* *

View File

@@ -10,6 +10,7 @@ import { logActivity } from '@/lib/userTracking';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu'; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
import { AssistantPanel, AssistantPanelProvider } from '@/components/ai-assistant'; import { AssistantPanel, AssistantPanelProvider } from '@/components/ai-assistant';
import OwliverChatBubble from '@/components/krow/OwliverChatBubble';
import { HoverCard, HoverCardTrigger, HoverCardContent } from '@/components/ui/hover-card'; import { HoverCard, HoverCardTrigger, HoverCardContent } from '@/components/ui/hover-card';
import KrowIdPreview from '@/components/krow/KrowIdPreview'; import KrowIdPreview from '@/components/krow/KrowIdPreview';
@@ -52,6 +53,9 @@ export default function Layout() {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [activeRole, setActiveRole] = useState(() => localStorage.getItem('activeRole') || 'employer'); const [activeRole, setActiveRole] = useState(() => localStorage.getItem('activeRole') || 'employer');
// Owliver's own open state. Owliver is a separate product with its own UI and
// voice workflow; it is not routed through the Krow AI panel.
const [owliverOpen, setOwliverOpen] = useState(false);
useEffect(() => { useEffect(() => {
base44.auth.me().then(setUser).catch(() => {}); base44.auth.me().then(setUser).catch(() => {});
@@ -89,13 +93,13 @@ export default function Layout() {
navigate(ROLE_HOME[role]); navigate(ROLE_HOME[role]);
}; };
// Talent can be sent to the Owliver profile-builder conversation from // Anything in the talent portal can open Owliver through this event — the
// anywhere via this event (the portal's own CTA uses it). // portal hero's own CTA uses it.
useEffect(() => { useEffect(() => {
const openBuilder = () => navigate('/owliver'); const openOwliver = () => setOwliverOpen(true);
window.addEventListener('krow:open-owliver', openBuilder); window.addEventListener('krow:open-owliver', openOwliver);
return () => window.removeEventListener('krow:open-owliver', openBuilder); return () => window.removeEventListener('krow:open-owliver', openOwliver);
}, [navigate]); }, []);
const navItems = ROLE_NAV[activeRole]; const navItems = ROLE_NAV[activeRole];
const isTalent = activeRole === 'talent'; const isTalent = activeRole === 'talent';
@@ -190,7 +194,7 @@ export default function Layout() {
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={() => navigate(isTalent ? '/owliver' : '/identity')} onClick={() => (isTalent ? window.dispatchEvent(new CustomEvent('krow:open-owliver')) : navigate('/identity'))}
className="cursor-pointer" className="cursor-pointer"
> >
<UserIcon className="w-4 h-4 mr-2" /> <UserIcon className="w-4 h-4 mr-2" />
@@ -314,7 +318,7 @@ export default function Layout() {
</Link> </Link>
))} ))}
<button <button
onClick={() => { setMenuOpen(false); navigate(isTalent ? '/owliver' : '/identity'); }} onClick={() => { setMenuOpen(false); isTalent ? window.dispatchEvent(new CustomEvent('krow:open-owliver')) : navigate('/identity'); }}
className={cn( className={cn(
'px-4 py-2.5 rounded-xl text-[14px] font-medium transition-all text-left w-full', 'px-4 py-2.5 rounded-xl text-[14px] font-medium transition-all text-left w-full',
isActive('/identity') ? 'bg-[#EFF6FF] text-[#0A39DF]' : 'text-[#374151] hover:bg-white/60' isActive('/identity') ? 'bg-[#EFF6FF] text-[#0A39DF]' : 'text-[#374151] hover:bg-white/60'
@@ -373,15 +377,19 @@ export default function Layout() {
<Outlet /> <Outlet />
</main> </main>
{/* Sticky below the sticky chrome so the assistant stays put while the {/* Krow AI — the dashboard assistant. Employer and Admin only; see
dashboard scrolls. Talent has no control bar, hence the two offsets. */} ai-assistant/placement.js. Sticky below the sticky chrome so it stays
put while the dashboard scrolls. */}
<AssistantPanel <AssistantPanel
stickyClassName={isTalent ? 'top-20' : 'top-[8.5rem]'} stickyClassName="top-[8.5rem]"
panelHeightClassName={ panelHeightClassName="h-[calc(100vh_-_10rem)]"
isTalent ? 'h-[calc(100vh_-_7rem)]' : 'h-[calc(100vh_-_10rem)]'
}
/> />
</div> </div>
{/* Owliver — the independent AI product, with its own branding, voice
experience and workflow. Talent only. Deliberately untouched by the
Krow AI work: different product, different UI, no shared components. */}
{isTalent && <OwliverChatBubble open={owliverOpen} onOpenChange={setOwliverOpen} />}
</div> </div>
</AssistantPanelProvider> </AssistantPanelProvider>
); );

View File

@@ -7,7 +7,6 @@ import CareerGrowth from '@/components/krow/talent/CareerGrowth';
import MarketOffers from '@/components/krow/talent/MarketOffers'; import MarketOffers from '@/components/krow/talent/MarketOffers';
import ExperienceNetwork from '@/components/krow/talent/ExperienceNetwork'; import ExperienceNetwork from '@/components/krow/talent/ExperienceNetwork';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { AssistantTrigger } from '@/components/ai-assistant';
export default function EmployeeDashboard() { export default function EmployeeDashboard() {
const { data: profile, isLoading } = useWorkerProfile(); const { data: profile, isLoading } = useWorkerProfile();
@@ -25,9 +24,6 @@ export default function EmployeeDashboard() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-end">
<AssistantTrigger />
</div>
<TalentHero profile={profile} jobRecs={jobRecs} nextCourse={nextCourse} openOwliver={openOwliver} /> <TalentHero profile={profile} jobRecs={jobRecs} nextCourse={nextCourse} openOwliver={openOwliver} />
<MarketOffers profile={profile} jobRecs={jobRecs} /> <MarketOffers profile={profile} jobRecs={jobRecs} />
<IncreaseMarketValue courses={courses} /> <IncreaseMarketValue courses={courses} />