From 1e9f245f7ca7a982190fcb78a99f1c2c8f84e80a Mon Sep 17 00:00:00 2001 From: Jakob Wennberg Date: Thu, 13 Aug 2026 11:19:45 +0200 Subject: [PATCH] refactor(agent): keep the assistant in the nav, FAB and underlag flow only (#1557) The founder wants the scattered per-page assistant buttons gone: the assistant is reachable from the nav and the floating tab everywhere, so in-page duplicates were noise. Removed the AgentSparkleButton call sites (year-end, verifikat detail, supplier invoice detail, invoice editor) and the now-orphaned component, the soft hand-off link in the Ny verifikat modal (plus its i18n keys), and the transaction-row overflow item. Kept: nav entry, floating tab, the Dokumentinkorg flow, and the sanctioned "Skapa med assistent" split-button mode on /bookkeeping (design.md convention 14). The command palette's hand-off entries hardcoded the agent name "Anna"; they now use the identity from AgentSheetProvider like every other affordance, and hide until agent onboarding is done (same gate as the FAB). Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 --- app/(dashboard)/bookkeeping/[id]/page.tsx | 9 --- app/(dashboard)/bookkeeping/year-end/page.tsx | 7 --- .../supplier-invoices/[id]/page.tsx | 7 --- components/agent/AgentSparkleButton.tsx | 62 ------------------- .../bookkeeping/NewJournalEntryDialog.tsx | 22 +------ components/common/CommandPalette.tsx | 44 +++++++------ components/invoices/InvoiceEditor.tsx | 6 -- .../transactions/TransactionInboxCard.tsx | 33 +--------- messages/en.json | 1 - messages/sv.json | 1 - 10 files changed, 27 insertions(+), 165 deletions(-) delete mode 100644 components/agent/AgentSparkleButton.tsx diff --git a/app/(dashboard)/bookkeeping/[id]/page.tsx b/app/(dashboard)/bookkeeping/[id]/page.tsx index 8f21e89c..5197e28b 100644 --- a/app/(dashboard)/bookkeeping/[id]/page.tsx +++ b/app/(dashboard)/bookkeeping/[id]/page.tsx @@ -27,7 +27,6 @@ import StrikeLinesDialog from '@/components/bookkeeping/StrikeLinesDialog' import CorrectMetadataDialog from '@/components/bookkeeping/CorrectMetadataDialog' import EditDraftEntryDialog from '@/components/bookkeeping/EditDraftEntryDialog' import RecordateEntryDialog from '@/components/bookkeeping/RecordateEntryDialog' -import AgentSparkleButton from '@/components/agent/AgentSparkleButton' import CorrectionChain from '@/components/bookkeeping/CorrectionChain' import RetagLineDialog, { type RetagLine } from '@/components/dimensions/RetagLineDialog' import { useCompanySettings } from '@/components/settings/useSettings' @@ -452,14 +451,6 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i {(entry.status === 'posted' || entry.status === 'draft') && (
- {entry.status === 'draft' && ( - - )} {entry.status === 'draft' && ( - ) -} diff --git a/components/bookkeeping/NewJournalEntryDialog.tsx b/components/bookkeeping/NewJournalEntryDialog.tsx index 17807ef9..1af3d529 100644 --- a/components/bookkeeping/NewJournalEntryDialog.tsx +++ b/components/bookkeeping/NewJournalEntryDialog.tsx @@ -1,7 +1,7 @@ 'use client' import { useTranslations } from 'next-intl' -import { Copy, Loader2, MessageCircle } from 'lucide-react' +import { Copy, Loader2 } from 'lucide-react' import { Dialog, DialogContent, @@ -9,7 +9,6 @@ import { DialogTitle, } from '@/components/ui/dialog' import JournalEntryForm, { type FormLine } from '@/components/bookkeeping/JournalEntryForm' -import { useAgentSheet } from '@/components/agent/AgentSheetProvider' export interface CopyPrefill { sourceId: string @@ -43,7 +42,6 @@ export default function NewJournalEntryDialog({ isLoading, }: Props) { const t = useTranslations('bookkeeping') - const { openAgentSheet, identity } = useAgentSheet() return ( @@ -63,24 +61,6 @@ export default function NewJournalEntryDialog({ {t('new_entry_dialog_title')} - {identity.isVerified && !copyPrefill && ( - // Hand off to the assistant: it reads the underlag (the figures the - // user often can't see), suggests accounts, and stages a balanced - // verifikat to approve: no copy-paste. Close the modal first so its - // focus trap doesn't fight the (non-modal) agent sheet. - - )} - {isLoading ? (
diff --git a/components/common/CommandPalette.tsx b/components/common/CommandPalette.tsx index 08bcc4da..a9291826 100644 --- a/components/common/CommandPalette.tsx +++ b/components/common/CommandPalette.tsx @@ -26,6 +26,7 @@ import { } from 'lucide-react' import { cn } from '@/lib/utils' import { useCompany } from '@/contexts/CompanyContext' +import { useAgentSheet } from '@/components/agent/AgentSheetProvider' import { requiredCapabilityForExtension } from '@/lib/entitlements/keys' type Entry = { @@ -93,6 +94,7 @@ export default function CommandPalette({ initialOpen = false }: { initialOpen?: const [activeIndex, setActiveIndex] = useState(0) const inputRef = useRef(null) const { capabilities } = useCompany() + const { identity } = useAgentSheet() // Drop entries that jump to a paywalled extension workspace the active // company can't reach (e.g. the AI-only Dokumentinkorg). The page itself is @@ -143,27 +145,31 @@ export default function CommandPalette({ initialOpen = false }: { initialOpen?: return q ? visible.filter(e => matches(e, q)) : visible.slice(0, 6) }, [q, allowedByCapability]) - const annaFallback: Entry | null = q && filteredActions.length === 0 && filteredPages.length === 0 - ? { - id: 'anna-fallback', - label: `Fråga Anna: "${query.trim()}"`, - icon: Wand2, - href: `/chat/new?prompt=${encodeURIComponent(query.trim())}`, - } - : q + // The hand-off-to-assistant entries use the agent name the user chose in + // /onboarding/agent, and hide entirely until that onboarding is done: the + // same gate as the nav entry and the FAB. + const assistantName = identity.displayName?.trim() || 'assistenten' + const assistantFallback: Entry | null = !identity.isVerified || !q + ? null + : filteredActions.length === 0 && filteredPages.length === 0 ? { - id: 'anna-followup', - label: `Fråga Anna istället: "${query.trim()}"`, + id: 'assistant-fallback', + label: `Fråga ${assistantName}: "${query.trim()}"`, + icon: Wand2, + href: `/chat/new?prompt=${encodeURIComponent(query.trim())}`, + } + : { + id: 'assistant-followup', + label: `Fråga ${assistantName} istället: "${query.trim()}"`, icon: Wand2, href: `/chat/new?prompt=${encodeURIComponent(query.trim())}`, } - : null const flatEntries: Entry[] = [ - ...(annaFallback && filteredActions.length === 0 && filteredPages.length === 0 ? [annaFallback] : []), + ...(assistantFallback && filteredActions.length === 0 && filteredPages.length === 0 ? [assistantFallback] : []), ...filteredActions, ...filteredPages, - ...(annaFallback && (filteredActions.length > 0 || filteredPages.length > 0) ? [annaFallback] : []), + ...(assistantFallback && (filteredActions.length > 0 || filteredPages.length > 0) ? [assistantFallback] : []), ] function commit(entry: Entry) { @@ -244,13 +250,13 @@ export default function CommandPalette({ initialOpen = false }: { initialOpen?: })} )} - {annaFallback && ( -
+ {assistantFallback && ( +
commit(annaFallback)} - onHover={() => setActiveIndex(flatEntries.indexOf(annaFallback))} + entry={assistantFallback} + active={flatEntries.indexOf(assistantFallback) === activeIndex} + onSelect={() => commit(assistantFallback)} + onHover={() => setActiveIndex(flatEntries.indexOf(assistantFallback))} />
)} diff --git a/components/invoices/InvoiceEditor.tsx b/components/invoices/InvoiceEditor.tsx index 5971cfb6..83c5f1ed 100644 --- a/components/invoices/InvoiceEditor.tsx +++ b/components/invoices/InvoiceEditor.tsx @@ -56,7 +56,6 @@ import { FirstInvoiceLogoPrompt } from '@/components/invoices/FirstInvoiceLogoPr import { useCompany, useCapability } from '@/contexts/CompanyContext' import { CAPABILITY } from '@/lib/entitlements/keys' import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' -import AgentSparkleButton from '@/components/agent/AgentSparkleButton' import { ROT_WORK_TYPES, RUT_WORK_TYPES, @@ -1470,11 +1469,6 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat {!bare &&

{subtitleText}

}
-
{isCopyMode && copyInitial && ( diff --git a/components/transactions/TransactionInboxCard.tsx b/components/transactions/TransactionInboxCard.tsx index bf559321..440358e0 100644 --- a/components/transactions/TransactionInboxCard.tsx +++ b/components/transactions/TransactionInboxCard.tsx @@ -17,7 +17,6 @@ import { FileSearch, Link2, Loader2, - MessageCircle, MoreHorizontal, Paperclip, Pencil, @@ -39,7 +38,6 @@ import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-exten const HAS_AI_EXTRACTION = ENABLED_EXTENSION_IDS.has('document-extraction') import { TransactionAttachmentIndicator } from './TransactionAttachmentIndicator' import { useCanWrite } from '@/lib/hooks/use-can-write' -import { useAgentSheet } from '@/components/agent/AgentSheetProvider' import type { TransactionWithInvoice, CategorizeHandler } from './transaction-types' interface TransactionInboxCardProps { @@ -107,11 +105,6 @@ export default function TransactionInboxCard({ // Attaching underlag is a write: hide the affordance from viewers so they // don't dead-end on a 403 (mirrors the gate in TransactionHistoryList). const { canWrite } = useCanWrite() - // The transaction-side entry point to the assistant ("Lena"). openAgentSheet - // hands this specific bank line to the transaction.categorization intent: - // the mirror of "Fråga assistenten" in Dokumentinkorgen, so the user can - // start a booking with the agent from the inbox they actually live in. - const { openAgentSheet, identity } = useAgentSheet() const isProcessing = processingId === transaction.id const isDisabled = processingId !== null && processingId !== transaction.id const isIncome = transaction.amount > 0 @@ -150,12 +143,6 @@ export default function TransactionInboxCard({ // Unbooked rows are still actionable (match, split, edit, categorize): that // includes imported bank rows, which are the whole point of the inbox. const isUnbooked = !transaction.journal_entry_id - // "Fråga [namn]" hands the row to the assistant for categorization/booking. - // Only on unbooked rows (nothing to categorize once it's a verifikat) and - // only after the user has built their agent in /onboarding/agent - // (identity.isVerified): same gate as the FAB / AgentSparkleButton. - const assistantName = identity.displayName?.trim() || 'min assistent' - const showAskAssistant = isUnbooked && identity.isVerified // ...but only rows the USER created in the app may be deleted. Imported rows // (bank sync / CSV) are ignore-only: mirrors the server guard in // DELETE /api/transactions/[id]. See lib/transactions/origin.ts. @@ -211,14 +198,7 @@ export default function TransactionInboxCard({ const showIgnoreItem = isUnbooked && isImportedTransaction(transaction) && !!onIgnore const showDeleteItem = canDelete && !!onDelete const showOverflowMenu = - showInvoiceMatchButton || showAskAssistant || showMatchVoucherItem || showAttachDocumentItem || showSplitItem || showEditItem || showIgnoreItem || showDeleteItem - - const askAssistant = () => - openAgentSheet({ - intentId: 'transaction.categorization', - intentArgs: { transaction_id: transaction.id }, - contextRef: `transaction:${transaction.id}`, - }) + showInvoiceMatchButton || showMatchVoucherItem || showAttachDocumentItem || showSplitItem || showEditItem || showIgnoreItem || showDeleteItem // The foldout carries row detail only (actions live on the row: pill + ⋯). // Rows with nothing to show don't expand at all; classified imported rows @@ -360,17 +340,6 @@ export default function TransactionInboxCard({ {invoiceMatchLabel} )} - {showAskAssistant && ( - { - e.stopPropagation() - askAssistant() - }} - > - - {`Fråga ${assistantName}`} - - )} {showMatchVoucherItem && ( { diff --git a/messages/en.json b/messages/en.json index 3ef302c9..5025e692 100644 --- a/messages/en.json +++ b/messages/en.json @@ -5412,7 +5412,6 @@ "tab_accounts": "Chart of accounts", "new_entry_dialog_title": "New journal entry", "create_with_assistant": "Create with assistant", - "ask_assistant_handoff": "Let the assistant fill it in?", "loading_source_voucher": "Loading source voucher...", "copy_failed_title": "Could not copy journal entry", "copy_source_missing": "Source voucher not found.", diff --git a/messages/sv.json b/messages/sv.json index f20185f7..fc8e88fa 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -5412,7 +5412,6 @@ "tab_accounts": "Kontoplan", "new_entry_dialog_title": "Ny verifikation", "create_with_assistant": "Skapa med assistent", - "ask_assistant_handoff": "Hellre låta assistenten fylla i?", "loading_source_voucher": "Laddar källverifikat...", "copy_failed_title": "Kunde inte kopiera verifikat", "copy_source_missing": "Källverifikatet hittades inte.",