Skip to content

V1 Back-Office AI Companion - UI Changes

Version: 1.1 Date: 2025-12-28 Related: V1_COMPANION_REQUIREMENTS.md, architecture/frontend.md


Overview

This document details the UI changes needed for the V1 Back-Office AI Companion. Changes apply primarily to the Staff App, with minimal changes to the shared UI library.


1. New Routes

Staff App Routes

The following routes are implemented in apps/staff-app/src/App.tsx:

// V1 Companion Routes (IMPLEMENTED)
{ path: 'clients/:clientId/returns/:returnId/upload', element: <DocumentUploadPage /> },
{ path: 'returns/:returnId/analysis', element: <AnalysisDashboardPage /> },

// V1 Companion Routes (DEFERRED)
// { path: 'clients/quick', element: <QuickClientPage /> },        // Using CreateClientPage instead
// { path: 'clients/:clientId/worksheet', element: <WorksheetPage /> },
// { path: 'clients/:clientId/worksheet/export', element: <WorksheetExportPage /> },

Note: Route structure changed from client-centric to return-centric for better workflow alignment.


2. New Pages

2.1 QuickClientPage - Quick Client Entry

Path: /clients/quick Purpose: Fast client creation with minimal fields

Wireframe:

┌─────────────────────────────────────────────────────────────────┐
│  ← Back                                     Tax Practice AI     │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ╔═══════════════════════════════════════════════════════════╗ │
│  ║              New Client - Quick Entry                      ║ │
│  ╠═══════════════════════════════════════════════════════════╣ │
│  ║                                                            ║ │
│  ║  Client Name *                                             ║ │
│  ║  ┌──────────────────────────────────────────────────────┐ ║ │
│  ║  │                                                      │ ║ │
│  ║  └──────────────────────────────────────────────────────┘ ║ │
│  ║                                                            ║ │
│  ║  Tax Year *                                                ║ │
│  ║  ┌──────────────────────────────────────────────────────┐ ║ │
│  ║  │ 2024                                           ▼    │ ║ │
│  ║  └──────────────────────────────────────────────────────┘ ║ │
│  ║                                                            ║ │
│  ║  Legacy Account Number (optional)                          ║ │
│  ║  ┌──────────────────────────────────────────────────────┐ ║ │
│  ║  │ Enter existing account # to link                     │ ║ │
│  ║  └──────────────────────────────────────────────────────┘ ║ │
│  ║  ℹ A new account number will be generated (A10001+)       ║ │
│  ║                                                            ║ │
│  ║  Notes (optional)                                          ║ │
│  ║  ┌──────────────────────────────────────────────────────┐ ║ │
│  ║  │                                                      │ ║ │
│  ║  │                                                      │ ║ │
│  ║  └──────────────────────────────────────────────────────┘ ║ │
│  ║                                                            ║ │
│  ║                          ┌────────────────────────────┐   ║ │
│  ║                          │   Create & Upload Docs     │   ║ │
│  ║                          └────────────────────────────┘   ║ │
│  ║                                                            ║ │
│  ╚════════════════════════════════════════════════════════════╝ │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Component:

// apps/staff-app/src/pages/QuickClientPage.tsx

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Card, CardHeader, CardTitle, CardContent } from '@tax-practice/ui';
import { Button, Input, Textarea, Select } from '@tax-practice/ui';
import { useQuickCreateClient } from '@/hooks/useClients';

const schema = z.object({
    display_name: z.string().min(1, 'Client name is required'),
    tax_year: z.number().min(2020).max(2030),
    legacy_account_number: z.string().optional(),
    notes: z.string().optional(),
});

type FormData = z.infer<typeof schema>;

export function QuickClientPage() {
    const navigate = useNavigate();
    const createMutation = useQuickCreateClient();

    const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
        resolver: zodResolver(schema),
        defaultValues: {
            tax_year: new Date().getFullYear(),
        },
    });

    const onSubmit = async (data: FormData) => {
        const client = await createMutation.mutateAsync(data);
        navigate(`/clients/${client.id}/upload`);
    };

    return (
        <div className="max-w-lg mx-auto mt-8">
            <Card>
                <CardHeader>
                    <CardTitle>New Client - Quick Entry</CardTitle>
                </CardHeader>
                <CardContent>
                    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
                        {/* Form fields */}
                    </form>
                </CardContent>
            </Card>
        </div>
    );
}


2.2 DocumentUploadPage - Enhanced Document Upload

Path: /clients/:clientId/upload Purpose: Drag-drop upload with folder import

Wireframe:

┌─────────────────────────────────────────────────────────────────┐
│  ← Back to Client                            Tax Practice AI    │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  John Smith (A10001)                              Tax Year 2024  │
│  Legacy: 12345                                                   │
│                                                                  │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                                                              ││
│  │              ┌────────────────────────────┐                 ││
│  │              │         📁                 │                 ││
│  │              │                            │                 ││
│  │              │   Drag files here          │                 ││
│  │              │   or click to browse       │                 ││
│  │              │                            │                 ││
│  │              │   PDF, JPEG, PNG, CSV      │                 ││
│  │              └────────────────────────────┘                 ││
│  │                                                              ││
│  │              ┌────────────────────────────┐                 ││
│  │              │   📂 Import Folder         │                 ││
│  │              └────────────────────────────┘                 ││
│  │                                                              ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                  │
│  Uploaded Documents (5)                                          │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ 📄 W2_Employer_ABC.pdf        W-2         ✓ Extracted       ││
│  │ 📄 1099-INT_Bank_XYZ.pdf      1099-INT    ✓ Extracted       ││
│  │ 📄 bank_statement_dec.pdf     Bank Stmt   ⏳ Processing     ││
│  │ 📄 receipt_001.jpg            Receipt     ✓ Extracted       ││
│  │ 📄 transactions.csv           CSV Data    ✓ Imported        ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                  │
│                          ┌────────────────────────────────┐     │
│                          │   Continue to Analysis →       │     │
│                          └────────────────────────────────┘     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Key Features: - Large drag-and-drop zone - "Import Folder" button that opens folder picker - Real-time upload progress - Document list with classification status - Continue button when ready


2.3 AnalysisDashboardPage - AI Analysis Dashboard

Path: /clients/:clientId/analysis Purpose: View AI insights, ask questions, add annotations

Wireframe:

┌─────────────────────────────────────────────────────────────────┐
│  ← Back │ John Smith (A10001)              [Generate Worksheet] │
├─────────────────────────────────────────────────────────────────┤
│  Summary │ Documents │ Insights │ Missing │ Prior Year │ Q&A   │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ╔═══════════════════════════════════════════════════════════╗  │
│  ║ SUMMARY                                                    ║  │
│  ╠═══════════════════════════════════════════════════════════╣  │
│  ║ Client: John Smith       Tax Year: 2024                   ║  │
│  ║ Account: A10001          Legacy: 12345                    ║  │
│  ║ Documents: 12            Processed: 12                    ║  │
│  ║                                                           ║  │
│  ║ Income Summary:                                           ║  │
│  ║   W-2 Wages:       $85,432.00  [W2_Employer_ABC.pdf]     ║  │
│  ║   Interest:         $1,234.56  [1099-INT_Bank_XYZ.pdf]   ║  │
│  ║   Dividends:          $567.89  [1099-DIV_Broker.pdf]     ║  │
│  ║   Total Income:    $87,234.45                            ║  │
│  ║                                                           ║  │
│  ║ Key Changes from Prior Year:                              ║  │
│  ║   • W-2 wages up 5.2% ($81,200 → $85,432)               ║  │
│  ║   • Interest income up 45% ($851 → $1,234)              ║  │
│  ║   • New: K-1 income not present in prior year            ║  │
│  ╚═══════════════════════════════════════════════════════════╝  │
│                                                                  │
│  ╔═══════════════════════════════════════════════════════════╗  │
│  ║ AI INSIGHTS                                     3 items    ║  │
│  ╠═══════════════════════════════════════════════════════════╣  │
│  ║ ⚠️ ATTENTION: K-1 shows significant partnership loss      ║  │
│  ║    ($12,340). Verify passive activity rules apply.        ║  │
│  ║    [Source: K1_Partnership_XYZ.pdf, Box 1]               ║  │
│  ║                                                           ║  │
│  ║ ℹ️ NOTE: Interest income significantly higher than PY.    ║  │
│  ║    Consider estimated tax implications for next year.     ║  │
│  ║    [Source: 1099-INT comparison to prior year]           ║  │
│  ║                                                           ║  │
│  ║ ✓ GOOD: All W-2 boxes reconcile correctly.               ║  │
│  ╚═══════════════════════════════════════════════════════════╝  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Tab Content:

Tab Content
Summary Client info, income summary, key changes
Documents Document list with click-to-view, extracted data
Insights AI observations, flags, warnings
Missing Expected documents not yet received
Prior Year Side-by-side comparison with prior year
Q&A Interactive AI chat interface

2.4 WorksheetPage - Worksheet Preview

Path: /clients/:clientId/worksheet Purpose: Preview generated worksheet before export

Wireframe:

┌─────────────────────────────────────────────────────────────────┐
│  ← Back │ Worksheet Preview                      [Export]       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                    TAX PREPARATION WORKSHEET                 ││
│  │                                                              ││
│  │ Client: John Smith                    Account: A10001       ││
│  │ Tax Year: 2024                        Prepared: 12/24/2024  ││
│  │ Legacy Account: 12345                                        ││
│  │ ──────────────────────────────────────────────────────────  ││
│  │                                                              ││
│  │ DOCUMENT INVENTORY                                           ││
│  │ ┌──────────────────────────────────────────────────────────┐││
│  │ │ Document              │ Type      │ Date     │ Status    │││
│  │ ├──────────────────────────────────────────────────────────┤││
│  │ │ W2_Employer_ABC.pdf   │ W-2       │ 12/15/24 │ Extracted │││
│  │ │ 1099-INT_Bank_XYZ.pdf │ 1099-INT  │ 12/16/24 │ Extracted │││
│  │ │ ...                                                       │││
│  │ └──────────────────────────────────────────────────────────┘││
│  │                                                              ││
│  │ EXTRACTED DATA                                               ││
│  │ ──────────────────────────────────────────────────────────  ││
│  │ Income:                                                      ││
│  │   W-2 Wages: $85,432.00                                     ││
│  │     [Source: W2_Employer_ABC.pdf, Box 1]                    ││
│  │   Federal Withholding: $12,814.80                           ││
│  │     [Source: W2_Employer_ABC.pdf, Box 2]                    ││
│  │   ...                                                        ││
│  │                                                              ││
│  │ ANNOTATIONS                                                  ││
│  │ ──────────────────────────────────────────────────────────  ││
│  │ [FLAG] K-1 loss needs passive activity review               ││
│  │        - Added by: jsmith@firm.com on 12/24/2024            ││
│  │ [NOTE] Client mentioned possible home office deduction      ││
│  │        - Added by: jsmith@firm.com on 12/24/2024            ││
│  │                                                              ││
│  │ AI ANALYSIS                                                  ││
│  │ ──────────────────────────────────────────────────────────  ││
│  │ Summary:                                                     ││
│  │ This return shows a 5.2% increase in W-2 income compared    ││
│  │ to prior year. Interest income increased significantly      ││
│  │ (45%), which may impact estimated tax payments...           ││
│  │                                                              ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                  │
└─────────────────────────────────────────────────────────────────┘


2.5 WorksheetExportPage - Export Options

Path: /clients/:clientId/worksheet/export Purpose: Select format and sections for export

Wireframe:

┌─────────────────────────────────────────────────────────────────┐
│  Export Worksheet                                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ╔═══════════════════════════════════════════════════════════╗  │
│  ║ Export Format                                              ║  │
│  ╠═══════════════════════════════════════════════════════════╣  │
│  ║                                                            ║  │
│  ║   ○ PDF (Recommended)                                     ║  │
│  ║     Formatted document ready for printing                  ║  │
│  ║                                                            ║  │
│  ║   ○ Excel                                                  ║  │
│  ║     Spreadsheet with multiple tabs                         ║  │
│  ║                                                            ║  │
│  ║   ○ Markdown                                               ║  │
│  ║     Plain text for easy copying                            ║  │
│  ║                                                            ║  │
│  ╚═══════════════════════════════════════════════════════════╝  │
│                                                                  │
│  ╔═══════════════════════════════════════════════════════════╗  │
│  ║ Include Sections                                           ║  │
│  ╠═══════════════════════════════════════════════════════════╣  │
│  ║                                                            ║  │
│  ║   ☑ Document Inventory                                    ║  │
│  ║   ☑ Extracted Data with Source Citations                  ║  │
│  ║   ☑ Annotations (Notes, Flags, Questions)                 ║  │
│  ║   ☑ AI Analysis                                           ║  │
│  ║   ☑ Prior Year Comparison                                 ║  │
│  ║   ☑ Open Items / Action Required                          ║  │
│  ║   ☐ Q&A History                                           ║  │
│  ║                                                            ║  │
│  ╚═══════════════════════════════════════════════════════════╝  │
│                                                                  │
│  ╔═══════════════════════════════════════════════════════════╗  │
│  ║ Options                                                    ║  │
│  ╠═══════════════════════════════════════════════════════════╣  │
│  ║                                                            ║  │
│  ║   ☑ Include source citations                              ║  │
│  ║   ☐ Include signature lines                               ║  │
│  ║                                                            ║  │
│  ╚═══════════════════════════════════════════════════════════╝  │
│                                                                  │
│                          ┌────────────────────────────────┐     │
│                          │   Generate & Download          │     │
│                          └────────────────────────────────┘     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘


3. New Components

3.1 ConnectivityBanner - Cloud Status Indicator

Purpose: Show S3/cloud connectivity status with fallback message

Location: packages/ui/src/components/common/ConnectivityBanner.tsx

// packages/ui/src/components/common/ConnectivityBanner.tsx

import { useState, useEffect } from 'react';
import { AlertTriangle, Wifi, WifiOff, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { api } from '@/lib/api';

export function ConnectivityBanner() {
    const [status, setStatus] = useState<'connected' | 'disconnected' | 'checking'>('checking');

    const checkConnectivity = async () => {
        setStatus('checking');
        try {
            await api.checkConnectivity();
            setStatus('connected');
        } catch {
            setStatus('disconnected');
        }
    };

    useEffect(() => {
        checkConnectivity();
        const interval = setInterval(checkConnectivity, 60000); // Check every 60s
        return () => clearInterval(interval);
    }, []);

    if (status === 'connected') {
        return null; // No banner when connected
    }

    return (
        <div className="bg-amber-50 border-b border-amber-200 px-4 py-3">
            <div className="flex items-center justify-between max-w-7xl mx-auto">
                <div className="flex items-center gap-3">
                    {status === 'checking' ? (
                        <RefreshCw className="h-5 w-5 text-amber-600 animate-spin" />
                    ) : (
                        <WifiOff className="h-5 w-5 text-amber-600" />
                    )}
                    <div>
                        <p className="text-sm font-medium text-amber-800">
                            Cloud Storage Unavailable
                        </p>
                        <p className="text-xs text-amber-700">
                            AI Companion features are still available. Documents cannot be stored
                            at this time. Keep files in your existing system as the source of truth.
                        </p>
                    </div>
                </div>
                <Button
                    variant="outline"
                    size="sm"
                    onClick={checkConnectivity}
                    disabled={status === 'checking'}
                >
                    <RefreshCw className={`h-4 w-4 mr-2 ${status === 'checking' ? 'animate-spin' : ''}`} />
                    Retry
                </Button>
            </div>
        </div>
    );
}

Usage in Layout:

// apps/staff-app/src/components/layout/MainLayout.tsx

import { ConnectivityBanner } from '@tax-practice/ui';

export function MainLayout() {
    return (
        <div className="min-h-screen">
            <ConnectivityBanner />
            <Header />
            <main>
                <Outlet />
            </main>
        </div>
    );
}


3.2 FolderImport - Folder Selection Component

Purpose: Allow importing documents from a local folder

Location: packages/ui/src/components/document/FolderImport.tsx

// packages/ui/src/components/document/FolderImport.tsx

import { useRef, useState } from 'react';
import { FolderOpen, Upload, X, Check, AlertCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Progress } from '@/components/ui/progress';

interface FolderImportProps {
    onImport: (files: File[]) => Promise<void>;
    supportedTypes?: string[];
}

export function FolderImport({ onImport, supportedTypes = ['.pdf', '.jpg', '.png', '.csv'] }: FolderImportProps) {
    const inputRef = useRef<HTMLInputElement>(null);
    const [isOpen, setIsOpen] = useState(false);
    const [preview, setPreview] = useState<{ supported: File[]; unsupported: string[] } | null>(null);
    const [importing, setImporting] = useState(false);
    const [progress, setProgress] = useState(0);

    const handleFolderSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
        const files = Array.from(e.target.files || []);

        const supported: File[] = [];
        const unsupported: string[] = [];

        files.forEach(file => {
            const ext = '.' + file.name.split('.').pop()?.toLowerCase();
            if (supportedTypes.includes(ext)) {
                supported.push(file);
            } else {
                unsupported.push(file.name);
            }
        });

        setPreview({ supported, unsupported });
        setIsOpen(true);
    };

    const handleConfirmImport = async () => {
        if (!preview) return;

        setImporting(true);
        setProgress(0);

        try {
            await onImport(preview.supported);
            setProgress(100);
            setTimeout(() => {
                setIsOpen(false);
                setPreview(null);
                setImporting(false);
            }, 500);
        } catch (error) {
            setImporting(false);
        }
    };

    return (
        <>
            <input
                ref={inputRef}
                type="file"
                webkitdirectory=""
                directory=""
                multiple
                className="hidden"
                onChange={handleFolderSelect}
            />

            <Button
                variant="outline"
                onClick={() => inputRef.current?.click()}
                className="w-full"
            >
                <FolderOpen className="h-4 w-4 mr-2" />
                Import Folder
            </Button>

            <Dialog open={isOpen} onOpenChange={setIsOpen}>
                <DialogContent>
                    <DialogHeader>
                        <DialogTitle>Import Folder</DialogTitle>
                    </DialogHeader>

                    {preview && (
                        <div className="space-y-4">
                            <div className="flex items-center gap-2 text-green-600">
                                <Check className="h-5 w-5" />
                                <span>{preview.supported.length} files will be imported</span>
                            </div>

                            {preview.unsupported.length > 0 && (
                                <div className="text-amber-600">
                                    <div className="flex items-center gap-2">
                                        <AlertCircle className="h-5 w-5" />
                                        <span>{preview.unsupported.length} unsupported files will be skipped</span>
                                    </div>
                                    <ul className="text-xs mt-2 ml-7 list-disc">
                                        {preview.unsupported.slice(0, 5).map(f => (
                                            <li key={f}>{f}</li>
                                        ))}
                                        {preview.unsupported.length > 5 && (
                                            <li>...and {preview.unsupported.length - 5} more</li>
                                        )}
                                    </ul>
                                </div>
                            )}

                            {importing && <Progress value={progress} />}

                            <div className="flex justify-end gap-2">
                                <Button variant="outline" onClick={() => setIsOpen(false)} disabled={importing}>
                                    Cancel
                                </Button>
                                <Button onClick={handleConfirmImport} disabled={importing || preview.supported.length === 0}>
                                    <Upload className="h-4 w-4 mr-2" />
                                    Import {preview.supported.length} Files
                                </Button>
                            </div>
                        </div>
                    )}
                </DialogContent>
            </Dialog>
        </>
    );
}

3.3 AnnotationPanel - Document Annotations

Purpose: Add and view annotations on documents

Location: packages/ui/src/components/document/AnnotationPanel.tsx

// packages/ui/src/components/document/AnnotationPanel.tsx

import { useState } from 'react';
import { Flag, MessageSquare, HelpCircle, CheckCircle, Edit3, Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';

type AnnotationType = 'NOTE' | 'FLAG' | 'QUESTION' | 'VERIFIED' | 'OVERRIDE';

interface Annotation {
    id: string;
    type: AnnotationType;
    content: string;
    source_reference?: string;
    created_by: string;
    created_at: string;
}

interface AnnotationPanelProps {
    documentId: string;
    annotations: Annotation[];
    onAdd: (annotation: { type: AnnotationType; content: string; source_reference?: string }) => Promise<void>;
}

const typeConfig: Record<AnnotationType, { icon: React.ReactNode; color: string; label: string }> = {
    NOTE: { icon: <MessageSquare className="h-4 w-4" />, color: 'bg-blue-100 text-blue-800', label: 'Note' },
    FLAG: { icon: <Flag className="h-4 w-4" />, color: 'bg-red-100 text-red-800', label: 'Flag' },
    QUESTION: { icon: <HelpCircle className="h-4 w-4" />, color: 'bg-amber-100 text-amber-800', label: 'Question' },
    VERIFIED: { icon: <CheckCircle className="h-4 w-4" />, color: 'bg-green-100 text-green-800', label: 'Verified' },
    OVERRIDE: { icon: <Edit3 className="h-4 w-4" />, color: 'bg-purple-100 text-purple-800', label: 'Override' },
};

export function AnnotationPanel({ documentId, annotations, onAdd }: AnnotationPanelProps) {
    const [isAdding, setIsAdding] = useState(false);
    const [newType, setNewType] = useState<AnnotationType>('NOTE');
    const [newContent, setNewContent] = useState('');
    const [submitting, setSubmitting] = useState(false);

    const handleSubmit = async () => {
        if (!newContent.trim()) return;

        setSubmitting(true);
        await onAdd({ type: newType, content: newContent });
        setNewContent('');
        setIsAdding(false);
        setSubmitting(false);
    };

    return (
        <div className="space-y-4">
            <div className="flex items-center justify-between">
                <h3 className="font-medium">Annotations</h3>
                <Button variant="outline" size="sm" onClick={() => setIsAdding(true)}>
                    <Plus className="h-4 w-4 mr-1" />
                    Add
                </Button>
            </div>

            {isAdding && (
                <div className="border rounded-lg p-3 space-y-3">
                    <Select value={newType} onValueChange={(v) => setNewType(v as AnnotationType)}>
                        <SelectTrigger>
                            <SelectValue />
                        </SelectTrigger>
                        <SelectContent>
                            {Object.entries(typeConfig).map(([key, config]) => (
                                <SelectItem key={key} value={key}>
                                    <div className="flex items-center gap-2">
                                        {config.icon}
                                        {config.label}
                                    </div>
                                </SelectItem>
                            ))}
                        </SelectContent>
                    </Select>

                    <Textarea
                        value={newContent}
                        onChange={(e) => setNewContent(e.target.value)}
                        placeholder="Enter your annotation..."
                        rows={3}
                    />

                    <div className="flex justify-end gap-2">
                        <Button variant="outline" size="sm" onClick={() => setIsAdding(false)}>
                            Cancel
                        </Button>
                        <Button size="sm" onClick={handleSubmit} disabled={submitting || !newContent.trim()}>
                            Save
                        </Button>
                    </div>
                </div>
            )}

            <div className="space-y-2">
                {annotations.map((ann) => {
                    const config = typeConfig[ann.type];
                    return (
                        <div key={ann.id} className="border rounded-lg p-3">
                            <div className="flex items-start gap-2">
                                <Badge className={config.color}>
                                    {config.icon}
                                    <span className="ml-1">{config.label}</span>
                                </Badge>
                                <p className="text-sm flex-1">{ann.content}</p>
                            </div>
                            <p className="text-xs text-muted-foreground mt-2">
                                {ann.created_by}  {new Date(ann.created_at).toLocaleString()}
                            </p>
                        </div>
                    );
                })}

                {annotations.length === 0 && !isAdding && (
                    <p className="text-sm text-muted-foreground text-center py-4">
                        No annotations yet
                    </p>
                )}
            </div>
        </div>
    );
}

3.4 AIChat - Q&A Interface

Purpose: Interactive AI chat with source citations

Location: packages/ui/src/components/ai/AIChat.tsx

// packages/ui/src/components/ai/AIChat.tsx

import { useState, useRef, useEffect } from 'react';
import { Send, Bot, User, FileText } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { ScrollArea } from '@/components/ui/scroll-area';

interface Message {
    id: string;
    role: 'user' | 'assistant';
    content: string;
    citations?: Array<{ document: string; page?: number; field?: string }>;
    timestamp: Date;
}

interface AIChatProps {
    clientId: string;
    onAsk: (question: string) => Promise<{ answer: string; citations?: Message['citations'] }>;
    initialMessages?: Message[];
}

export function AIChat({ clientId, onAsk, initialMessages = [] }: AIChatProps) {
    const [messages, setMessages] = useState<Message[]>(initialMessages);
    const [input, setInput] = useState('');
    const [loading, setLoading] = useState(false);
    const scrollRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
        scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
    }, [messages]);

    const handleSend = async () => {
        if (!input.trim() || loading) return;

        const userMessage: Message = {
            id: Date.now().toString(),
            role: 'user',
            content: input,
            timestamp: new Date(),
        };

        setMessages(prev => [...prev, userMessage]);
        setInput('');
        setLoading(true);

        try {
            const response = await onAsk(input);

            const assistantMessage: Message = {
                id: (Date.now() + 1).toString(),
                role: 'assistant',
                content: response.answer,
                citations: response.citations,
                timestamp: new Date(),
            };

            setMessages(prev => [...prev, assistantMessage]);
        } catch (error) {
            const errorMessage: Message = {
                id: (Date.now() + 1).toString(),
                role: 'assistant',
                content: 'Sorry, I encountered an error processing your question. Please try again.',
                timestamp: new Date(),
            };
            setMessages(prev => [...prev, errorMessage]);
        }

        setLoading(false);
    };

    const handleKeyDown = (e: React.KeyboardEvent) => {
        if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            handleSend();
        }
    };

    return (
        <div className="flex flex-col h-full">
            <ScrollArea className="flex-1 p-4">
                <div className="space-y-4">
                    {messages.length === 0 && (
                        <div className="text-center py-8 text-muted-foreground">
                            <Bot className="h-12 w-12 mx-auto mb-3 opacity-50" />
                            <p>Ask me anything about this client's documents.</p>
                            <p className="text-sm mt-2">Examples:</p>
                            <ul className="text-sm mt-1 space-y-1">
                                <li>"Compare wages to last year"</li>
                                <li>"What documents are missing?"</li>
                                <li>"Summarize all 1099 income"</li>
                            </ul>
                        </div>
                    )}

                    {messages.map((msg) => (
                        <div
                            key={msg.id}
                            className={`flex gap-3 ${msg.role === 'user' ? 'justify-end' : ''}`}
                        >
                            {msg.role === 'assistant' && (
                                <div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
                                    <Bot className="h-4 w-4 text-primary" />
                                </div>
                            )}

                            <div className={`max-w-[80%] ${msg.role === 'user' ? 'bg-primary text-primary-foreground' : 'bg-muted'} rounded-lg p-3`}>
                                <p className="text-sm whitespace-pre-wrap">{msg.content}</p>

                                {msg.citations && msg.citations.length > 0 && (
                                    <div className="mt-2 pt-2 border-t border-current/10">
                                        <p className="text-xs opacity-70 mb-1">Sources:</p>
                                        {msg.citations.map((cite, i) => (
                                            <div key={i} className="flex items-center gap-1 text-xs opacity-70">
                                                <FileText className="h-3 w-3" />
                                                <span>
                                                    {cite.document}
                                                    {cite.page && `, p.${cite.page}`}
                                                    {cite.field && `, ${cite.field}`}
                                                </span>
                                            </div>
                                        ))}
                                    </div>
                                )}
                            </div>

                            {msg.role === 'user' && (
                                <div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center">
                                    <User className="h-4 w-4 text-primary-foreground" />
                                </div>
                            )}
                        </div>
                    ))}

                    {loading && (
                        <div className="flex gap-3">
                            <div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
                                <Bot className="h-4 w-4 text-primary animate-pulse" />
                            </div>
                            <div className="bg-muted rounded-lg p-3">
                                <div className="flex gap-1">
                                    <span className="w-2 h-2 bg-current rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
                                    <span className="w-2 h-2 bg-current rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
                                    <span className="w-2 h-2 bg-current rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
                                </div>
                            </div>
                        </div>
                    )}

                    <div ref={scrollRef} />
                </div>
            </ScrollArea>

            <div className="border-t p-4">
                <div className="flex gap-2">
                    <Textarea
                        value={input}
                        onChange={(e) => setInput(e.target.value)}
                        onKeyDown={handleKeyDown}
                        placeholder="Ask about this return..."
                        className="min-h-[44px] max-h-32 resize-none"
                        rows={1}
                    />
                    <Button onClick={handleSend} disabled={loading || !input.trim()}>
                        <Send className="h-4 w-4" />
                    </Button>
                </div>
            </div>
        </div>
    );
}

4. Navigation Changes

Staff App Sidebar

Add new quick-access items:

// apps/staff-app/src/components/layout/Sidebar.tsx

const navItems = [
    { icon: LayoutDashboard, label: 'Dashboard', path: '/' },
    { icon: Users, label: 'Clients', path: '/clients' },
    { icon: UserPlus, label: 'Quick Entry', path: '/clients/quick' },  // NEW
    { icon: FileStack, label: 'Queue', path: '/queue' },
    { icon: FileText, label: 'E-Filing', path: '/efiling' },
    { icon: DollarSign, label: 'Invoices', path: '/invoices' },
    { icon: Settings, label: 'Admin', path: '/admin', roles: ['admin'] },
];

5. API Hooks

New React Query hooks for V1 features:

// apps/staff-app/src/hooks/useV1.ts

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { api } from '@tax-practice/ui';

// Quick create client
export function useQuickCreateClient() {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: (data: { display_name: string; tax_year: number; legacy_account_number?: string; notes?: string }) =>
            api.post('/v1/clients/quick', data),
        onSuccess: () => {
            queryClient.invalidateQueries({ queryKey: ['clients'] });
        },
    });
}

// Folder import
export function useFolderImport(clientId: string) {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: (files: File[]) => api.uploadMultipleDocuments(clientId, files),
        onSuccess: () => {
            queryClient.invalidateQueries({ queryKey: ['documents', clientId] });
        },
    });
}

// AI Analysis
export function useAnalysis(clientId: string) {
    return useQuery({
        queryKey: ['analysis', clientId],
        queryFn: () => api.get(`/v1/clients/${clientId}/analysis`),
        staleTime: 1000 * 60 * 5,
    });
}

// Annotations
export function useAnnotations(documentId: string) {
    return useQuery({
        queryKey: ['annotations', documentId],
        queryFn: () => api.get(`/v1/documents/${documentId}/annotations`),
    });
}

export function useAddAnnotation(documentId: string) {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: (data: { type: string; content: string }) =>
            api.post(`/v1/documents/${documentId}/annotations`, data),
        onSuccess: () => {
            queryClient.invalidateQueries({ queryKey: ['annotations', documentId] });
        },
    });
}

// Worksheet
export function useGenerateWorksheet(clientId: string) {
    return useQuery({
        queryKey: ['worksheet', clientId],
        queryFn: () => api.get(`/v1/clients/${clientId}/worksheet`),
        enabled: false, // Only fetch on demand
    });
}

export function useExportWorksheet(clientId: string) {
    return useMutation({
        mutationFn: (options: { format: string; sections: string[] }) =>
            api.post(`/v1/clients/${clientId}/worksheet/export`, options),
    });
}

// Connectivity check
export function useConnectivity() {
    return useQuery({
        queryKey: ['connectivity'],
        queryFn: () => api.get('/v1/system/connectivity'),
        refetchInterval: 60000,
        retry: false,
    });
}

Summary

Component Type Location
QuickClientPage Page apps/staff-app/src/pages/
DocumentUploadPage Page apps/staff-app/src/pages/
AnalysisDashboardPage Page apps/staff-app/src/pages/
WorksheetPage Page apps/staff-app/src/pages/
WorksheetExportPage Page apps/staff-app/src/pages/
ConnectivityBanner Component packages/ui/src/components/common/
FolderImport Component packages/ui/src/components/document/
AnnotationPanel Component packages/ui/src/components/document/
AIChat Component packages/ui/src/components/ai/

Document History

Version Date Author Changes
1.0 2024-12-24 Claude Initial V1 UI changes documentation