'use client' import { useState } from 'react' import { Pencil, X, Loader2, ArrowLeft, ArrowRight } from 'lucide-react' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { cn } from '@/lib/utils' import { AVATAR_OPTIONS } from '@/components/agent/avatars' import AgentAvatar from '@/components/agent/AgentAvatar' import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message' interface InitialFields { entity_type_label: string sni_codes: { code: string; name: string }[] purpose: string | null city: string | null fiscal_period: string | null vat_period: string | null f_skatt: string | null employees: string | null } interface ProfilePayload { company_id: string horizontal_atoms: string[] vertical_atoms: string[] modifier_atoms: string[] is_multi_vertical: boolean profile_summary: string // Still carried from the composer + stored on the profile row, but no // longer surfaced as a form step here: the Phase C chat intake owns the // questions now (reads them server-side). Kept on the type so the payload // shape stays aligned with the stream event. verification_questions: string[] uncertainty_notes: string[] composer_model: string composed_at: string } interface Props { companyId: string companyName: string initialFields: InitialFields // Pre-fetched atom titles from agent_atom_registry: used to render chips // with the authored title instead of a naive slug-derived label. Missing // ids fall through to deriveSlugTitle which is intentionally minimal. atomTitles: Record profile: ProfilePayload | null onVerified: () => void } // Maps an atom id to a chip label. Prefers the registry title when known. function atomLabel(id: string, atomTitles: Record): string { if (atomTitles[id]) return atomTitles[id] const slug = id.split('/').slice(-1)[0] ?? id return slug .split('-') .map((w) => { const upper = w.toUpperCase() if (upper === 'VAT' || upper === 'SRU' || upper === 'SIE' || upper === 'IT') return upper return w.length > 0 ? w[0].toUpperCase() + w.slice(1) : w }) .join(' ') } export default function ReviewCard({ companyId, companyName, initialFields, atomTitles, profile, onVerified, }: Props) { // Field-edit state. The pencil affordances let the user override anything // the composer inferred. Each override is sent to PATCH /api/agent/profile, // which stamps an overridden_at timestamp. const [fields, setFields] = useState(initialFields) const [editing, setEditing] = useState(null) const [summary, setSummary] = useState(profile?.profile_summary ?? '') const [editingSummary, setEditingSummary] = useState(false) const [horizontal, setHorizontal] = useState(profile?.horizontal_atoms ?? []) const [vertical, setVertical] = useState(profile?.vertical_atoms ?? []) const [modifier, setModifier] = useState(profile?.modifier_atoms ?? []) // Agent identity: name shown on the FAB, avatar shown alongside. const [displayName, setDisplayName] = useState('') const [avatarId, setAvatarId] = useState(AVATAR_OPTIONS[0].id) const [seedMemory, setSeedMemory] = useState('') const [verifying, setVerifying] = useState(false) const [verifyError, setVerifyError] = useState(null) // Two steps now: // 1: meet your assistant (name + avatar) // 2: agree on the facts (profile + specialties + form fields + optional // seed note), then "kör" which hands off to the Phase C chat intake. // The verification-question interview that used to live here as a form // stepper is gone: the chat conducts the real interview instead. type Step = 1 | 2 const [step, setStep] = useState(1) const totalPositions = 2 const currentPosition = step - 1 const agentName = displayName.trim() || 'din assistent' async function handleVerify() { setVerifying(true) setVerifyError(null) try { // Persist edits before verifying. Skipped if nothing changed. const changedFields: Record = {} for (const key of Object.keys(initialFields) as (keyof InitialFields)[]) { if (fields[key] !== initialFields[key]) { changedFields[key] = fields[key] } } const atomsChanged = !arrEq(horizontal, profile?.horizontal_atoms ?? []) || !arrEq(vertical, profile?.vertical_atoms ?? []) || !arrEq(modifier, profile?.modifier_atoms ?? []) const summaryChanged = summary !== (profile?.profile_summary ?? '') const trimmedName = displayName.trim() // Identity is always persisted on first verify so the FAB picks it up // immediately. If the user typed nothing, we leave display_name null // (UI falls back to "min revisor"). const identityChanged = trimmedName.length > 0 || avatarId !== AVATAR_OPTIONS[0].id if (Object.keys(changedFields).length > 0 || atomsChanged || summaryChanged || identityChanged) { const patchBody: Record = { company_id: companyId } if (Object.keys(changedFields).length > 0) patchBody.field_overrides = changedFields if (atomsChanged) { patchBody.atoms = { horizontal_atoms: horizontal, vertical_atoms: vertical, modifier_atoms: modifier, } } if (summaryChanged) patchBody.profile_summary = summary if (identityChanged) { patchBody.display_name = trimmedName.length > 0 ? trimmedName : null patchBody.avatar_id = avatarId } const res = await fetch('/api/agent/profile', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patchBody), }) if (!res.ok) { // Map the parsed body plus the status, never the raw response text: // the route answers thrown errors with the canonical envelope // `{ error: { code, message } }`, and throwing the raw JSON text // (or "[object Object]") discards the route's own Swedish reason. const body = await res.json().catch(() => null) setVerifyError(getUserErrorMessage(body, { statusCode: res.status })) return } } if (seedMemory.trim().length > 1) { await fetch('/api/agent/memory', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ company_id: companyId, content: seedMemory.trim(), kind: 'fact', source: 'user_taught', source_ref: 'onboarding_seed', relevance_score: 1.0, }), }) } const verifyRes = await fetch('/api/agent/profile/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ company_id: companyId }), }) if (!verifyRes.ok) { const body = await verifyRes.json().catch(() => null) setVerifyError(getUserErrorMessage(body, { statusCode: verifyRes.status })) return } onVerified() } catch (err) { setVerifyError(err instanceof Error ? getUserErrorMessage(err) : 'Kunde inte verifiera.') } finally { setVerifying(false) } } const stepTitle = step === 1 ? 'Träffa din assistent' : 'Stäm av detaljerna' const stepSubtitle = step === 1 ? 'Ge din assistent ett namn och välj en avatar.' : 'Bekräfta att uppgifterna stämmer, eller ändra det som blivit fel. Sen lär din assistent känna dig i en kort intervju.' return (

{companyName}

{stepTitle}

{stepSubtitle}

{/* Progress: one segment per step. Back navigation lives on the "Tillbaka" button below. */}