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 <noreply@anthropic.com>
This commit is contained in:
@@ -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') && (
|
||||
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
||||
{entry.status === 'draft' && (
|
||||
<AgentSparkleButton
|
||||
intentId="verifikation.draft"
|
||||
intentArgs={{ journal_entry_id: id }}
|
||||
contextRef={`verifikation:${id}`}
|
||||
className="w-full sm:w-auto"
|
||||
/>
|
||||
)}
|
||||
{entry.status === 'draft' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -7,7 +7,6 @@ import { EmptyState } from '@/components/ui/empty-state'
|
||||
import { ContextPicker } from '@/components/common/ContextPicker'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { CalendarPlus, Check, Lock } from 'lucide-react'
|
||||
import AgentSparkleButton from '@/components/agent/AgentSparkleButton'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { getErrorMessage } from '@/lib/errors/get-error-message'
|
||||
@@ -218,12 +217,6 @@ export default function YearEndPage() {
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<h1 className="font-display text-2xl leading-8 tracking-tight">Årsbokslut</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<AgentSparkleButton
|
||||
intentId="bokslut.step"
|
||||
intentArgs={{ step_id: null }}
|
||||
contextRef="bokslut:overview"
|
||||
size="default"
|
||||
/>
|
||||
{showWizard && periods && periods.length > 0 && step !== 'result' && (
|
||||
<ContextPicker
|
||||
items={periods.map((p) => ({
|
||||
|
||||
@@ -14,7 +14,6 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { getErrorMessage } from '@/lib/errors/get-error-message'
|
||||
import { ArrowLeft, CheckCircle, CreditCard, FileText, Trash2, Lock, Undo2, Info, Pencil, Plus, CalendarClock, Paperclip } from 'lucide-react'
|
||||
import AgentSparkleButton from '@/components/agent/AgentSparkleButton'
|
||||
import LinkVoucherPicker from '@/components/invoices/LinkVoucherPicker'
|
||||
import { useCanWrite } from '@/lib/hooks/use-can-write'
|
||||
import { formatDate, cn } from '@/lib/utils'
|
||||
@@ -526,12 +525,6 @@ export default function SupplierInvoiceDetailPage() {
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<AgentSparkleButton
|
||||
intentId="supplier_invoice.review"
|
||||
intentArgs={{ supplier_invoice_id: invoice.id }}
|
||||
contextRef={`supplier_invoice:${invoice.id}`}
|
||||
size="default"
|
||||
/>
|
||||
{/* Attest keys off approved_at, not the status: the overdue cron
|
||||
flips unbooked invoices to 'overdue' just by aging, and gating on
|
||||
'registered' alone left them with no way through attest (#1206). */}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAgentSheet } from './AgentSheetProvider'
|
||||
|
||||
interface Props {
|
||||
intentId: string
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
intentArgs?: Record<string, any>
|
||||
contextRef?: string
|
||||
// Override the auto-derived "Fråga [namn]" label when the page wants
|
||||
// something more contextual (e.g. "Förklara denna siffra"). Most surfaces
|
||||
// should leave this unset.
|
||||
label?: string
|
||||
size?: 'sm' | 'default' | 'lg'
|
||||
variant?: 'outline' | 'default' | 'ghost' | 'secondary'
|
||||
className?: string
|
||||
}
|
||||
|
||||
// Single source of truth for the in-page "Fråga [namn]" affordance. Every
|
||||
// page-header button across the dashboard (invoice form, supplier invoice,
|
||||
// bookkeeping, year-end, VAT report, KPI, …) routes through this component
|
||||
// so they share the same icon size, label format, spacing, and resolved
|
||||
// agent name. The transaction-row icon button stays separate: it's a
|
||||
// different UX (icon-only, ghost) embedded inside a row's action group.
|
||||
export default function AgentSparkleButton({
|
||||
intentId,
|
||||
intentArgs,
|
||||
contextRef,
|
||||
label,
|
||||
size = 'sm',
|
||||
variant = 'outline',
|
||||
className,
|
||||
}: Props) {
|
||||
const { openAgentSheet, identity } = useAgentSheet()
|
||||
// Same gate as AgentTrigger: hide all "Fråga …" affordances until the
|
||||
// user has finished /onboarding/agent.
|
||||
if (!identity.isVerified) return null
|
||||
const name = identity.displayName?.trim() || 'min assistent'
|
||||
const resolvedLabel = label ?? `Fråga ${name}`
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn('shrink-0', className)}
|
||||
onClick={() =>
|
||||
openAgentSheet({
|
||||
intentId,
|
||||
intentArgs,
|
||||
contextRef,
|
||||
})
|
||||
}
|
||||
>
|
||||
<MessageCircle className="mr-2 h-4 w-4" />
|
||||
{resolvedLabel}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -63,24 +61,6 @@ export default function NewJournalEntryDialog({
|
||||
<DialogTitle>{t('new_entry_dialog_title')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{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.
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onOpenChange(false)
|
||||
openAgentSheet({ intentId: 'verifikation.draft', contextRef: 'verifikation:new' })
|
||||
}}
|
||||
className="inline-flex w-fit items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<MessageCircle className="h-3.5 w-3.5" />
|
||||
{t('ask_assistant_handoff')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center gap-2 py-12 text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
|
||||
@@ -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<HTMLInputElement>(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?:
|
||||
})}
|
||||
</Section>
|
||||
)}
|
||||
{annaFallback && (
|
||||
<Section title="Anna">
|
||||
{assistantFallback && (
|
||||
<Section title={assistantName}>
|
||||
<Row
|
||||
entry={annaFallback}
|
||||
active={flatEntries.indexOf(annaFallback) === activeIndex}
|
||||
onSelect={() => commit(annaFallback)}
|
||||
onHover={() => setActiveIndex(flatEntries.indexOf(annaFallback))}
|
||||
entry={assistantFallback}
|
||||
active={flatEntries.indexOf(assistantFallback) === activeIndex}
|
||||
onSelect={() => commit(assistantFallback)}
|
||||
onHover={() => setActiveIndex(flatEntries.indexOf(assistantFallback))}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
</Heading>
|
||||
{!bare && <p className="text-muted-foreground">{subtitleText}</p>}
|
||||
</div>
|
||||
<AgentSparkleButton
|
||||
intentId="invoice.draft"
|
||||
intentArgs={{ customer_id: watchCustomerId ?? null }}
|
||||
contextRef={watchCustomerId ? `customer:${watchCustomerId}` : 'invoice:new'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isCopyMode && copyInitial && (
|
||||
|
||||
@@ -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}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{showAskAssistant && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
askAssistant()
|
||||
}}
|
||||
>
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
{`Fråga ${assistantName}`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{showMatchVoucherItem && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user