Luma

Light for your business

AI Personal Assistant for the Kasha Platform — A unified natural-language interface to 149 business operations across 13 domains

Executive Summary

Problem: The Kasha Platform (33 repos, 18 microservices, 10 frontend apps) has no unified natural-language interface. Users must navigate between 10 different UIs to perform 149 distinct business operations across scheduling, invoicing, banking, CRM, website management, and more. The existing AI assistant ("Heidi") is limited to the website builder domain only.

Solution: Luma AI Personal Assistant

A general-purpose AI Personal Assistant that gives every Kasha user a single conversational interface to the entire platform. Users can say "Book a haircut for Sarah at 2pm tomorrow" or "Send invoice #42 to the client" from any app.

149
Capabilities
13
Domains
10
Frontend Apps
18
Microservices

Coverage Domains

Scheduling Client Management Invoicing Banking Website Communications Tasks Business Config Documents Search KYC E-commerce Analytics

Delivery Approach

  • Phased rollout in 5 phases over 18-23 weeks
  • Phase 1 (Foundation): 17 read-only capabilities, streaming responses
  • Phase 2 (Read-Write): 57 total capabilities with confirmation flow
  • Phase 3 (Advanced): 107 capabilities with multi-step workflows
  • Phase 4 (Full Rollout): All 149 capabilities
  • Phase 5 (Polish): Analytics, feedback, performance optimization

Name Research: Why "Luma"

Selection Criteria

  • Short (1-3 syllables, ideally 2)
  • Easy to pronounce in English, German, French, Italian, Spanish, Arabic, Mandarin, Hindi
  • Gender-neutral
  • No negative connotations in any major language
  • Not trademarked by major AI products
  • Friendly, capable, professional — like a trusted colleague

Winner: Luma — Overall Score: 9.25/10

Evaluation Breakdown

Dimension Score Details
Pronounceability 10/10 LOO-ma. Very easy across all target languages. The "L" and "M" sounds are universal.
Memorability 9/10 Short, distinctive, evokes "luminous" and "illuminate"
Cultural Safety 9/10 No negative meanings found across 14 languages checked
Brand Fit 9/10 Perfect metaphor: light/illumination brings clarity to business operations

Etymology & Meaning

  • Latin: "lumen" (light) — Luma derives from this root
  • Brand Metaphor: "Illuminating business decisions"
  • Tagline: "Light for your business"

Why Luma Wins

  • Perfect metaphor: Light = clarity, guidance, understanding
  • Brand harmony: Kasha (earthy/warm/nourishing) + Luma (light/clarity) creates complementary contrast
  • Clean trademark landscape: No major PA/assistant AI conflicts (Luma AI exists for 3D/video, different category)
  • Gender-neutral: No gender associations in any language
  • Phonetically universal: L-U-M-A uses sounds that exist in virtually every language
  • Professional yet warm: Not robotic, childish, or overly casual

Top 5 Candidates Evaluated

Rank Name Score Key Meaning Status
1 Luma 9.25 Light (Latin) SELECTED
2 Nima 8.75 Just/Fair (Persian), Blessing (Arabic) Runner-up
3 Dara 8.75 Wisdom (Hebrew) Runner-up
4 Nara 8.50 Happy (Celtic), Historic city (Japanese) Alternative
5 Remi 8.50 Remedy (Latin), Comfort (Yoruba) Conflict: Remi AI

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
  Frontend (any of 10 Next.js apps)                          
  ┌──────────────────────────────────────────────────────┐   
    lr-luma-ui (iframe, like existing chat widget)         
    - Chat UI, streaming text, rich cards                  
    - Socket.IO client (namespace /luma)                   
  └──────────────────────────────────────────────────────┘   
└─────────────────┬──────────────────────┬────────────────────┘
                   REST (messages)        Socket.IO (streaming)
                                        
┌─────────────────────────────────────────────────────────────┐
  lr-api-gateway                                             
  ┌────────────────┐  ┌───────────────────────────────────┐  
   LumaController    LumaGateway (WS namespace /luma)    
   REST endpoints     Consumes from RabbitMQ stream       
   Guards + Auth      queue, emits to Socket.IO rooms     
  └───────┬────────┘  └───────────────────┬───────────────┘  
           RabbitMQ send()                 RabbitMQ consume 
└──────────┼────────────────────────────────┼──────────────────┘
                                           
┌─────────────────────────────────────────────────────────────┐
  lr-luma-service (NEW NestJS microservice)                  
  ┌─────────────┐  ┌──────────────┐  ┌───────────────────┐  
   Orchestrator │→│ LLM (Claude) │→│ Tool Registry        
   Agentic loop    Tool calling     149 tools, 13       
                   Streaming        domains              
  └──────┬──────┘  └──────────────┘  └───────────────────┘  
                                                             
  ┌──────┴──────┐  ┌──────────────┐  ┌───────────────────┐  
   Executor       Confirmation    Context Memory      
   RBAC-scoped    Risk-based      Entity tracking     
   17 RabbitMQ    confirmations   Topic continuity    
   ClientProxy                    Vector Memory       
                                  (pgvector / Qdrant)  
  └──────┬──────┘  └──────────────┘  └───────────────────┘  
                                                             
          Publishes stream tokens to RabbitMQ exchange ──────┤
└─────────┼────────────────────────────────────────────────────┘
           RabbitMQ send() to 17 services
          
┌─────────────────────────────────────────────────────────────┐
  Existing Microservices                                      
  booking / crm / invoicing / banking / notifications / ...   
└─────────────────────────────────────────────────────────────┘

Key Architectural Decisions

Decision Choice Rationale
LLM Provider Anthropic Claude (Sonnet) Differentiates from Heidi (GPT), native tool calling, handles large tool sets well
Tool approach Manual per-domain files from capability map Quality descriptions need human curation; 13 domain files
Cross-service comms Direct RabbitMQ (17 ClientProxy instances) Established platform pattern; avoids gateway HTTP overhead
Streaming RabbitMQ exchange → Gateway Socket.IO Matches AppNotificationsGateway pattern exactly
Message initiation REST POST (not Socket.IO) Allows existing guard stack (JWT, Permissions, Subscription)
Conversation storage MongoDB Variable-length messages, nested context; matches Heidi pattern
Usage tracking PostgreSQL Structured, aggregatable, extends BaseEntity
Frontend delivery iframe (new lr-luma-ui app) Matches existing chat widget pattern; zero host-app changes
UI placement Floating widget + full page Widget on all apps; full page in lr-onboarding-ui for power users
RBAC enforcement Three-layer: tool filter → pre-exec check → service guard Defense in depth; Claude never sees unauthorized tools; services remain final authority
Vector memory pgvector (Phase 1–3), Qdrant (Phase 4+) No new infra initially; abstraction layer enables migration at scale
Embedding model text-embedding-3-small (1024d) Cost-effective, fast, sufficient quality for conversation retrieval

Backend Service: lr-luma-service

Module Structure

lr-luma-service/
├── src/
│   ├── main.ts                     # Bootstrap + RabbitMQ microservice
│   ├── app.module.ts
│   ├── config/                     # Environment config (Anthropic keys, RabbitMQ, etc.)
│   ├── modules/
│   │   ├── conversation/           # MongoDB: conversation CRUD, message storage
│   │   │   ├── entities/conversation.entity.ts
│   │   │   ├── conversation.service.ts
│   │   │   └── conversation.controller.ts  # RPC handlers
│   │   ├── llm/                    # Anthropic Claude SDK wrapper
│   │   │   ├── llm.interface.ts    # Provider abstraction
│   │   │   ├── anthropic.provider.ts  # Claude with streaming + tool calling
│   │   │   └── llm.service.ts     # Retry logic, token tracking
│   │   ├── orchestrator/           # Central brain: message → intent → tool → response
│   │   │   └── orchestrator.service.ts
│   │   ├── tools/                  # Tool definitions + registry
│   │   │   ├── tools.registry.ts   # Central registry with permission filtering
│   │   │   └── domains/           # 13 domain files
│   │   │       ├── scheduling.tools.ts
│   │   │       ├── client-management.tools.ts
│   │   │       ├── invoicing.tools.ts
│   │   │       ├── banking.tools.ts
│   │   │       ├── website.tools.ts
│   │   │       ├── communications.tools.ts
│   │   │       ├── tasks.tools.ts
│   │   │       ├── business-config.tools.ts
│   │   │       ├── documents.tools.ts
│   │   │       ├── search.tools.ts
│   │   │       ├── kyc.tools.ts
│   │   │       ├── ecommerce.tools.ts
│   │   │       └── analytics.tools.ts
│   │   ├── executor/               # Calls downstream services via RabbitMQ
│   │   │   └── executor.service.ts # 17 ClientProxy instances, 15s timeout per RPC
│   │   ├── confirmation/           # Risk-based confirmation flow
│   │   │   └── confirmation.service.ts
│   │   ├── context/                # Conversation context memory stack
│   │   │   └── context.service.ts  # Entity tracking, topic detection, 10min decay
│   │   ├── memory/                 # Vector memory engine (RAG)
│   │   │   ├── memory.module.ts    # Qdrant/pgvector provider config
│   │   │   ├── memory.service.ts   # Retrieve/store operations
│   │   │   ├── memory.interface.ts # MemoryChunk, MemoryQuery types
│   │   │   ├── embedding.service.ts # Embedding model abstraction
│   │   │   ├── chunking.service.ts  # Conversation → embeddable chunks
│   │   │   └── memory-maintenance.service.ts # TTL, compaction, re-embed
│   │   ├── usage/                  # PostgreSQL: token tracking, rate limiting
│   │   │   └── entities/usage-log.entity.ts
│   │   └── moderation/             # Input/output content safety
│   └── common/
│       └── services/               # 17 RabbitMQ ClientProxy factory providers

LLM Integration (Anthropic Claude)

  • Package: @anthropic-ai/sdk
  • Model: claude-sonnet-4-20250514 (configurable via env)
  • Tool Calling: Native tools parameter (not JSON-in-prompt like Heidi)
  • Streaming: messages.stream() yields events published to RabbitMQ exchange
  • Temperature: 0.3 (reliable tool calling)
  • Max tokens: 4096

Tool System

Each tool is defined with:

interface ToolDefinition {
  name: string;                    // e.g. 'scheduling_create_appointment'
  description: string;             // For Claude's tool selection
  inputSchema: Record<string, any>; // JSON Schema
  domain: ToolDomain;              // 13 domains
  riskLevel: RiskLevel;            // None | Low | Medium | High | Critical
  targetService: string;           // RabbitMQ queue constant
  rpcCommand: string;              // e.g. 'create_event'
  operation: 'read' | 'write';
  requiredPermissions?: string[];
  confirmationTemplate?: string;
}

The ToolsRegistry provides getAccessibleTools(userPermissions, subscriptionFeatures) to filter tools per-user before sending to Claude.

Orchestrator Loop

  1. Receive user message
  2. Load conversation history + context
  3. Build system prompt with context, user info, current date/time
  4. Call Claude with messages + accessible tools
  5. If Claude returns tool_use:
    • a. Validate tool exists and input schema
    • b. Check risk level → if Medium/High/Critical, create PendingAction, stream confirmation prompt, STOP
    • c. If None/Low, execute via Executor, feed tool_result back to Claude
    • d. Loop (max 10 tool calls per message)
  6. If Claude returns text, stream to user
  7. Update conversation context (entity references, topic)
  8. Persist messages to MongoDB

Confirmation Flow

  • PendingAction stored in the conversation's MongoDB document
  • 5-minute TTL with auto-expiry
  • Risk levels:
    • None/Low — Execute immediately
    • Medium — Brief confirmation
    • High — Detailed preview
    • Critical — Explicit typed confirmation
  • User responds via REST POST /confirm → Luma executes or cancels the pending action

Conversation Entity (MongoDB)

{
  _id: ObjectId,
  businessId: string,
  userId: string,
  messages: ConversationMessage[],
  context: {
    activeBusiness, lastClient, lastInvoice,
    lastAppointment, conversationTopic, pendingAction
  },
  summary?: { text, summarizedUpToIndex },  // Auto-summarize after 20 messages
  title?: string,                            // Auto-generated from first message
  totalInputTokens: number,
  totalOutputTokens: number,
  status: 'active' | 'archived',
  createdAt, updatedAt
}

Security (Triple Layer)

  1. Tool filtering: Claude only sees tools the user has permission to use
  2. Pre-execution check: Verify permissions again before each RPC call
  3. Target service guards: Downstream services enforce their own business logic and multi-tenancy

RBAC & Permission-Aware AI

Principle: Luma never sees, mentions, or acts on data the current user doesn't have access to. Every interaction is scoped by role, permissions, business tenancy, and subscription tier — enforced at three separate layers before a response ever reaches the user.

Role Hierarchy

Kasha's existing RBAC model is inherited by Luma. The role determines which tools are injected into the Claude context and what data scoping is applied to every query.

Role Scope Luma Behavior
Owner Full business All 149 tools available. Can read/write all entities, view analytics, manage billing, configure business settings.
Admin Full business (no billing) All tools except billing/subscription management. Full read access to all entities.
Manager Assigned departments Read/write for assigned domains only. Cannot access business config, billing, or cross-department analytics.
Staff Own assignments Read own clients, appointments, tasks. Write limited to own schedule, task status updates. No financial data.
Client (Portal) Own entity only Read own invoices, appointments, documents. Request new bookings. No access to other clients, staff, or internal data.

Three-Layer Enforcement

┌─────────────────────────────────────────────────────────────────────┐
│  LAYER 1: Tool Injection Filter                                    │
│  ToolsRegistry.getAccessibleTools(user.permissions, user.features) │
│  → Claude only SEES tools the user can use                         │
│  → No tool = no hallucinated action                                │
│  → Subscription tier further limits available tools                │
│                                                                     │
│  Example: Client portal user → 12 tools (not 149)                  │
│  Example: Staff member → 38 tools (own-scoped)                     │
│  Example: Owner → 149 tools (full access)                          │
└────────────────────────────────┬────────────────────────────────────┘
                                 │
┌────────────────────────────────▼────────────────────────────────────┐
│  LAYER 2: Pre-Execution Permission Check                           │
│  executor.validatePermissions(user, tool, params)                  │
│  → Re-verify permission BEFORE executing RPC call                  │
│  → Inject businessId + userId into every query                     │
│  → Validate entity ownership (e.g., "is this the user's invoice?") │
│  → Reject with friendly Luma message, not a raw error              │
└────────────────────────────────┬────────────────────────────────────┘
                                 │
┌────────────────────────────────▼────────────────────────────────────┐
│  LAYER 3: Downstream Service Guards                                │
│  Target microservice enforces its own @Permissions() decorators,   │
│  multi-tenancy WHERE clauses, and business logic                   │
│  → Even if Layers 1 & 2 fail, the service rejects unauthorized     │
│  → Each service is the final authority on its own data             │
└─────────────────────────────────────────────────────────────────────┘

Tool-Level Permission Mapping

Every tool definition includes a requiredPermissions array that maps to Kasha's existing permission system:

// Example: scheduling domain tools with permission requirements
{
  name: 'scheduling_list_appointments',
  requiredPermissions: ['scheduling:read'],
  operation: 'read',
  dataScope: 'role-based',        // Owner/Admin=all, Staff=own, Client=own
  // ...
},
{
  name: 'scheduling_create_appointment',
  requiredPermissions: ['scheduling:write'],
  operation: 'write',
  riskLevel: 'Medium',
  dataScope: 'role-based',
  // ...
},
{
  name: 'invoicing_view_revenue_report',
  requiredPermissions: ['invoicing:read', 'analytics:read'],
  operation: 'read',
  dataScope: 'admin-only',        // Only Owner/Admin roles
  // ...
}

Data Scoping per Role

When Luma queries downstream services, the Executor automatically injects scope filters based on the user's role:

// executor.service.ts — scope injection before every RPC call

private scopeParams(user: LumaUser, tool: ToolDefinition, params: any): any {
  const scoped = { ...params, businessId: user.businessId };

  switch (tool.dataScope) {
    case 'own-only':
      // Client portal & staff: restrict to own entities
      scoped.userId = user.id;
      scoped.clientId = user.clientId;     // for portal users
      break;

    case 'department':
      // Manager: restrict to assigned departments
      scoped.departmentIds = user.assignedDepartments;
      break;

    case 'role-based':
      // Apply userId filter only for Staff/Client roles
      if (['staff', 'client'].includes(user.role)) {
        scoped.userId = user.id;
      }
      break;

    case 'admin-only':
      // No additional filter — but tool is already filtered out for non-admins
      break;
  }

  return scoped;
}

System Prompt Role Context

The system prompt injected into every Claude call includes the user's role context, ensuring Luma never refers to data or actions beyond the user's scope:

// Injected into system prompt dynamically per request

## Current User Context
- Name: {{user.firstName}} {{user.lastName}}
- Role: {{user.role}} ({{user.roleDescription}})
- Business: {{business.name}} (ID: {{business.id}})
- Permissions: {{user.permissions.join(', ')}}
- Subscription: {{business.subscriptionTier}}

## Behavioral Boundaries
- You can ONLY discuss and act on data this user has access to.
- NEVER reference other clients, staff members, or entities outside this user's scope.
- If the user asks about data they don't have access to, explain politely that
  they don't have permission and suggest contacting their administrator.
- For Client Portal users: address them as a customer, not an internal user.
  Never reveal internal operations, staff names, or business configuration.

Client Portal Isolation

When Luma operates in the client portal context (end-customer view), the experience is fundamentally different:

Aspect Admin/Staff View Client Portal View
Available tools 38–149 (role-dependent) 12 (own data only)
Persona "Your AI business assistant" "Your personal account assistant"
Data visible All clients, invoices, bookings Own invoices, own bookings only
Actions Create, update, delete entities View, request booking, download invoice
Financial data Revenue, margins, reports Own outstanding balance only
Quick actions New invoice, assign task, etc. Book appointment, check balance

Audit Trail

Every Luma action is logged with full user context for compliance and debugging:

{
  timestamp: ISODate,
  businessId: string,
  userId: string,
  userRole: string,
  conversationId: ObjectId,
  action: 'tool_executed' | 'tool_denied' | 'confirmation_requested' | 'confirmation_approved',
  toolName: string,
  params: Record<string, any>,     // sanitized, no PII
  result: 'success' | 'denied' | 'error',
  denyReason?: string,              // e.g., 'insufficient_permissions', 'entity_not_owned'
  executionTimeMs: number
}

Vector Memory & Context Engine

Purpose: Give Luma persistent, semantic memory across conversations. Each user and company gets an isolated context window — a growing knowledge base of past interactions, preferences, entity relationships, and business patterns that Luma retrieves via RAG (Retrieval-Augmented Generation) to deliver increasingly relevant responses over time.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│  User sends message                                             │
└──────────────────────────┬──────────────────────────────────────┘
                           ▼
┌──────────────────────────────────────────────────────────────────┐
│  1. EMBED query                                                  │
│     Embedding model: text-embedding-3-small (OpenAI)             │
│     or voyage-3-lite (Anthropic Voyage)                          │
│     Dimension: 1024                                              │
└──────────────────────────┬───────────────────────────────────────┘
                           ▼
┌──────────────────────────────────────────────────────────────────┐
│  2. RETRIEVE from vector store                                   │
│     Qdrant (self-hosted) or pgvector (PostgreSQL extension)      │
│     Query: user message embedding                                │
│     Filter: { businessId, userId } (tenant isolation)            │
│     Top-K: 5 most relevant chunks                                │
│     Score threshold: 0.72                                        │
└──────────────────────────┬───────────────────────────────────────┘
                           ▼
┌──────────────────────────────────────────────────────────────────┐
│  3. INJECT into Claude system prompt                             │
│     Retrieved context → "## Relevant History" section            │
│     Combined with: conversation history + user role context      │
│     → Claude sees the full picture without loading all history   │
└──────────────────────────┬───────────────────────────────────────┘
                           ▼
┌──────────────────────────────────────────────────────────────────┐
│  4. STORE new interaction                                        │
│     After response: embed the exchange (user msg + Luma reply)   │
│     Chunk strategy: per-exchange (not per-message)               │
│     Metadata: timestamp, topic, entities referenced, tool calls  │
│     Write to same tenant-scoped collection                       │
└──────────────────────────────────────────────────────────────────┘

Module Structure

lr-luma-service/
└── src/modules/
    └── memory/
        ├── memory.module.ts            # Qdrant/pgvector provider, embedding config
        ├── memory.service.ts           # Core retrieve/store operations
        ├── memory.interface.ts         # MemoryChunk, MemoryQuery, MemoryResult
        ├── embedding.service.ts        # Embedding model abstraction
        ├── chunking.service.ts         # Conversation → chunks with metadata
        └── memory-maintenance.service.ts  # TTL cleanup, re-embedding, compaction

Tenant Isolation Strategy

Every vector is scoped by businessId and optionally userId, ensuring complete data isolation between tenants and between users within the same tenant.

// Collection/namespace strategy

// Option A: Qdrant — one collection, filtered by payload
Collection: "luma_memory"
Payload fields (indexed):
  - businessId: keyword     (mandatory filter)
  - userId: keyword          (optional filter — for personal context)
  - scope: keyword           (values: 'personal' | 'company')
  - topic: keyword           (e.g., 'invoicing', 'scheduling')
  - timestamp: integer       (for recency weighting)

// Option B: pgvector — same PostgreSQL, new table
CREATE TABLE luma_memory_vectors (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  business_id   UUID NOT NULL REFERENCES businesses(id),
  user_id       UUID REFERENCES users(id),       -- NULL = company-wide context
  scope         VARCHAR(20) NOT NULL,             -- 'personal' | 'company'
  content       TEXT NOT NULL,                    -- original text chunk
  embedding     vector(1024) NOT NULL,            -- pgvector column
  metadata      JSONB,                            -- topic, entities, tool_calls
  created_at    TIMESTAMPTZ DEFAULT NOW(),
  expires_at    TIMESTAMPTZ,                      -- optional TTL

  -- Tenant isolation index
  CONSTRAINT idx_memory_tenant ON (business_id, user_id)
);

-- HNSW index for fast similarity search
CREATE INDEX idx_memory_embedding ON luma_memory_vectors
  USING hnsw (embedding vector_cosine_ops);

What Gets Stored

Not every message is stored — the memory engine is selective about what enters the vector store:

Source Scope Example
Conversation summaries Personal "User asked about Alpine Tech GmbH invoice #2847 and requested a payment reminder"
User preferences Personal "User prefers invoice reminders sent on Mondays", "User's default currency is CHF"
Entity relationships Company "Alpine Tech GmbH — key client, 14 invoices, primary contact: Hans Mueller"
Decision patterns Company "Booking requests from this client typically need 2-hour slots on Wednesdays"
Tool usage patterns Personal "User frequently checks banking balance on Friday afternoons"
Error resolutions Company "Invoice sync with banking failed — resolved by re-linking the bank account"

Context Window Management

Claude has a finite context window. The memory engine manages what goes in:

// orchestrator.service.ts — context assembly

async buildContext(user: LumaUser, message: string, conversation: Conversation) {
  const contextBudget = 8000;  // tokens reserved for retrieved context
  let usedTokens = 0;

  // 1. Current conversation history (most recent 10 messages)
  const recentHistory = conversation.messages.slice(-10);
  usedTokens += estimateTokens(recentHistory);

  // 2. Conversation summary (if > 20 messages, auto-summarized)
  if (conversation.summary) {
    usedTokens += estimateTokens(conversation.summary.text);
  }

  // 3. Vector memory retrieval (fill remaining budget)
  const remainingBudget = contextBudget - usedTokens;
  const memories = await this.memoryService.retrieve({
    query: message,
    businessId: user.businessId,
    userId: user.id,
    maxTokens: remainingBudget,
    topK: 5,
    minScore: 0.72,
    scopes: ['personal', 'company'],  // personal results ranked higher
  });

  // 4. Assemble system prompt section
  return {
    recentHistory,
    summary: conversation.summary?.text,
    relevantMemory: memories.map(m => m.content),
  };
}

Memory Lifecycle

┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│   CAPTURE    │───▶│    EMBED     │───▶│    STORE     │───▶│   RETRIEVE   │
│              │    │              │    │              │    │              │
│ Conversation │    │ text → vec   │    │ Qdrant /     │    │ Semantic     │
│ ends or hits │    │ 1024-dim     │    │ pgvector     │    │ search on    │
│ 20+ messages │    │ embedding    │    │ with tenant  │    │ next query   │
│              │    │              │    │ metadata     │    │              │
└──────────────┘    └──────────────┘    └──────┬───────┘    └──────────────┘
                                               │
                                        ┌──────▼───────┐
                                        │   MAINTAIN   │
                                        │              │
                                        │ TTL expiry   │
                                        │ Compaction   │
                                        │ Re-embed on  │
                                        │ model change │
                                        └──────────────┘
  • Capture: At conversation end or every 20 messages, the chunking service extracts meaningful exchanges
  • Embed: Each chunk is embedded using the configured embedding model (1024 dimensions)
  • Store: Vector + metadata written to the tenant-scoped collection
  • Retrieve: On every new message, top-K similar chunks are retrieved and injected into context
  • Maintain: Background job handles TTL expiry (default 180 days), compaction of redundant memories, and re-embedding when the model changes

RBAC Integration

Vector memory respects the same permission model as the rest of Luma:

  • Personal memories are scoped to userId — only the user who created them can retrieve them
  • Company memories are scoped to businessId — accessible to all users in the business with sufficient role
  • Client portal users can only retrieve memories from their own interactions — never from admin/staff conversations about them
  • Memory retrieval applies the same data scoping filters as tool execution (Layer 2)
  • Admin conversations about a client (internal notes, strategy) are stored as company-scope and never surfaced to the client's portal

Technology Decision

Option Pros Cons Recommendation
pgvector Already have PostgreSQL; no new infra; ACID transactions; familiar tooling Performance degrades at >1M vectors; limited ANN algorithm options Phase 1–3 Start here
Qdrant Purpose-built; fast at scale; payload filtering; built-in tenant isolation Additional infrastructure; operational overhead; one more service to manage Phase 4+ Migrate when scale demands

The memory.service.ts abstraction layer makes the underlying store swappable without changing the orchestrator or any upstream code.

API Gateway Integration

New Files in lr-api-gateway

src/luma/
  luma.module.ts          # Module with RabbitMQ exchange/queue bindings
  luma.controller.ts      # REST endpoints
  luma.service.ts         # Proxy to lr-luma-service via RabbitMQ
  luma.gateway.ts         # Socket.IO namespace /luma
  luma.const.ts           # Events, room naming helpers
  dto/                    # CreateConversation, SendMessage, ConfirmAction DTOs

REST Endpoints

Method Endpoint Description
POST /api/:businessId/luma/conversations Start conversation
GET /api/:businessId/luma/conversations List conversations
GET /api/:businessId/luma/conversations/:id Get with messages
DELETE /api/:businessId/luma/conversations/:id Delete conversation
POST /api/:businessId/luma/conversations/:id/messages Send message (triggers stream)
POST /api/:businessId/luma/conversations/:id/confirm Confirm/cancel pending action

All endpoints use: @UseGuards(JwtAuthGuard, PermissionsAuthGuard, SubscriptionGuard) with @AuthPermissions(PermissionsEnum.ACCESS_LUMA).

Streaming Flow

  1. User sends message via REST POST → returns 202 { messageId, status: 'processing' }
  2. Gateway forwards to lr-luma-service via RabbitMQ send()
  3. Luma service processes and publishes stream tokens to e.lr-luma-streaming exchange
  4. Gateway's LumaGateway consumes from q.lr-luma-streaming queue (matching AppNotificationsGateway pattern)
  5. Gateway emits tokens to Socket.IO room luma:{userId}:{conversationId}
  6. Final stream_complete event carries full assembled message

Socket.IO Events

Event Direction Purpose
luma.join_conversation Client → Server Join conversation room
luma.leave_conversation Client → Server Leave room
luma.stream_token Server → Client Streaming text chunk
luma.stream_complete Server → Client Stream finished with full message
luma.stream_error Server → Client Stream failed
luma.action_confirmation_required Server → Client Needs user confirmation
luma.disambiguation_required Server → Client Needs user selection
luma.action_executed Server → Client Action completed
luma.typing Server → Client Luma is thinking

Framework Changes

File Change
lr-be-framework/commons/constants/services-names.const.ts Add LR_LUMA_SERVICE
lr-be-framework RabbitMQ enums Add streaming exchange/queue/route
lr-api-gateway/src/common/enums/service-provider-tokens.enum.ts Add LUMA_SERVICE
lr-api-gateway/src/auth/enums/permissions.enum.ts Add ACCESS_LUMA
lr-api-gateway/src/app.module.ts Import LumaModule

Frontend: lr-luma-ui

Delivery: iframe (matching existing chat widget pattern)

  • New Next.js 15 app: lr-luma-ui
  • Embedded as an iframe via <LumaWidget /> in UserProvider (alongside existing <ChatWidget />)
  • postMessage communication for context passing from parent apps
  • Zero changes needed in individual apps — Luma appears everywhere automatically

Chat Interface

  • Floating widget: 56x56px button (bottom-right), expands to 400x600px panel
  • Full-page mode: /luma route in lr-onboarding-ui for power users
  • Components: Message bubbles, streaming text renderer, typing indicator, rich response cards, quick action chips, context indicator, conversation list

Rich Response Cards

Card Purpose
ConfirmationCard Yes/No with action details (red styling for high-risk)
DisambiguationCard Numbered clickable options
AppointmentCard Date, time, client, service + Confirm/Reschedule/Cancel
InvoiceCard Line items, totals, VAT + Create/Edit/Cancel
BalanceCard Account balance + recent transactions
ClientCard Name, email, phone, last appointment
ProgressCard Multi-step workflow progress bar
ErrorCard Error description + recovery action options

Context-Aware Behavior

Parent apps broadcast page context to the Luma iframe via postMessage on route changes:

{ appModule: 'invoices', entityType: 'invoice', entityId: 'INV-089', ... }

Luma uses this to:

  • Show contextual greetings ("I see you're looking at Invoice #89...")
  • Offer relevant quick actions (Send, Mark Paid, Create Reminder)
  • Pre-fill entity references when the user makes requests

Framework Additions

File Change
lr-fe-framework/components/luma/luma-widget.tsx Iframe wrapper component
lr-fe-framework/components/providers/user-provider.tsx Add <LumaWidget />
lr-fe-framework/hooks/use-socket.tsx Add Luma = '/luma' to SocketNamespace
lr-fe-framework/hooks/use-luma.ts Context broadcasting, open/close helpers
lr-fe-framework/lib/utils/luma.utils.ts openLuma() utility (like openChatWidget())

10. Phased Rollout

Luma will be delivered in 5 phases over approximately 18-24 weeks, progressively building capability from read-only queries to full platform control.

1

Foundation

4-6 weeks
Goal: Working Luma widget on all apps with read-only capabilities and streamed text responses.

Backend Deliverables:

  • Scaffold lr-luma-service with config, MongoDB, PostgreSQL
  • LLM module with Anthropic provider (streaming)
  • Tool registry with 3 demo domains: scheduling (read), client management (read), search
  • Conversation CRUD
  • Executor with 5 service proxies (booking, CRM, search, invoicing, banking)
  • Gateway module with REST endpoints + Socket.IO + streaming queue

Frontend Deliverables:

  • Scaffold lr-luma-ui Next.js app
  • Basic chat UI (message list, composer, typing indicator, streaming text)
  • LumaWidget iframe component in lr-fe-framework
  • Socket.IO integration for /luma namespace
Capabilities Enabled: 17 of 149
User Can: "What's my balance?", "Show today's appointments", "Find client Sarah"
2

Core Read-Write

4-6 weeks
Goal: Most common write operations with confirmation flow and rich response cards.

Backend Deliverables:

  • Orchestrator with full agentic loop (tool call → execute → feed result)
  • Confirmation module with risk-based flow
  • Context module with entity tracking and topic detection
  • Remaining tool definitions (all 13 domains)
  • Remaining 12 service proxies

Frontend Deliverables:

  • Confirmation and disambiguation cards
  • Appointment, invoice, balance, client cards
  • Context indicator and quick actions
  • Context broadcasting from parent apps
New Capabilities: ~40 additional (total ~57)
User Can: "Book Sarah at 2pm tomorrow", "Create invoice for £500", "Send payment reminder"
3

Advanced Features

4-6 weeks
Goal: Multi-step workflows, remaining write capabilities, error recovery.

Backend Deliverables:

  • Multi-step workflow orchestration
  • Conversation summarization (auto-summarize after 20 messages)
  • All remaining tool definitions

Frontend Deliverables:

  • Progress cards for multi-step workflows
  • Error cards with recovery actions
  • Full-page mode in lr-onboarding-ui
  • Conversation history list
  • Quick commands (power user shorthand syntax)
New Capabilities: ~50 additional (total ~107)
User Can: "Transfer £1000 to my savings", "Publish my website", "Send bulk SMS to clients"
4

Full Rollout

3-4 weeks
Goal: All 149 capabilities across all 13 domains.

New Capability Domains:

  • Documents & Contracts (12 capabilities)
  • Business Config (17 capabilities)
  • Tasks & Notes (5 capabilities)
  • KYC/Compliance (5 capabilities)
  • E-commerce (10 capabilities)
New Capabilities: 42 remaining (total 149)
User Can: "Create contract for Sarah", "Update VAT settings", "Upload passport for verification"
5

Polish & Analytics

3-4 weeks
Goal: Analytics, feedback, performance, A/B testing.

Deliverables:

  • PostHog integration for usage tracking
  • Thumbs up/down feedback on responses
  • Performance optimization (lazy loading, message virtualization)
  • Conversation search
  • User preferences
  • Keyboard shortcuts (Ctrl+K to open Luma)
  • Onboarding tour for first-time users
Production Ready: Full analytics dashboard, user feedback loop, optimal performance

11. Capability Map

Luma provides 149 distinct capabilities across 13 business domains, connecting natural language to RabbitMQ microservices.

Domain Read Capabilities Write Capabilities Total
Scheduling & Availability 8 14 22
Client Management 5 8 13
Invoicing & Quotes 7 9 16
Banking & Payments 5 9 14
Website & Content 6 14 20
Communications 3 4 7
Tasks & Notes 2 3 5
Business Config 6 11 17
Documents & Contracts 5 7 12
Search & Lookup 2 0 2
Compliance (KYC) 2 3 5
E-commerce 5 5 10
Analytics & Insights 6 0 6
TOTAL 62 87 149

Risk Classification Matrix

Risk Level Examples Confirmation Required Reversible
None Search, list, view, check balance, analytics No N/A
Low Mark notification read, update own profile No Yes
Medium Create client, create invoice (draft), update settings, block time Brief confirmation Yes (soft-delete, draft state)
High Send invoice, send contract, make payment, cancel appointment Detailed confirmation with preview Partially (credit note, reschedule)
Critical Delete records, make large payments, cancel subscription, close account Explicit confirmation with safeguard No

12. System Prompt

Luma's core identity and operational protocols, embedded in every conversation.

# ROLE: LUMA, THE KASHA BUSINESS ASSISTANT
You are Luma, the intelligent personal assistant for Kasha, a comprehensive B2B SaaS platform. Your goal is to help business owners manage their operations across 13 domains: Scheduling, Client Management, Invoicing, Banking, Website, Communications, Tasks, Business Config, Documents, Search, KYC, E-commerce, and Analytics.

# OPERATIONAL PROTOCOLS
1. TOOL USAGE: You execute actions via specialized tool calls. Do not explain that you are "using a tool"; simply provide the result or ask for confirmation.
2. CONFIRMATION HIERARCHY:
   - READ (GET/LIST): Execute immediately.
   - WRITE (CREATE/UPDATE/DELETE): Always present a summary and ask: "Shall I go ahead with this?"
   - HIGH-RISK (Bank Transfers, Deletions, Mass Emails): Provide a detailed preview. Use bold text for amounts, dates, and names. Require explicit "Confirm" or "Yes".
3. DISAMBIGUATION: If a user's request matches multiple entities (e.g., "Sarah Smith" vs. "Sarah Jones"), present the options clearly with identifying details (email or last activity) and ask for clarification.
4. CONTEXT MEMORY: Maintain a "Working Context." Remember the last referenced client, invoice, or appointment. Use relative terms like "her invoice" or "that booking" correctly.
5. ERROR HANDLING:
   - Unknown Request: "I don't have the capability to [action] yet. However, I can help you with [related Kasha domain]."
   - Permission Denied: "It looks like I don't have permission to access that. You may need to check your role settings in the Business Config."
   - Service Errors: "I'm having trouble connecting to the [Service Name] right now. Please try again in a moment or let me know if I can help with something else."

# TONE & STYLE
- Professional, warm, and proactive.
- Use business terminology (e.g., "reconcile," "remittance," "availability," "lead conversion").
- Be concise. Avoid "As an AI..." or "Certainly, I can help with that."
- Address the user as a partner in their business growth.

# DOMAIN KNOWLEDGE (149 CAPABILITIES)
- Refer to the platform as "Kasha."
- When creating content (emails, invoices), align with the user's established business voice.

13. Conversation Flow Examples

Real-world conversation patterns demonstrating read operations, write confirmations, disambiguation, multi-step workflows, and error recovery.

Example 1: Simple Read - Banking Balance

What is my balance?
Your current Kasha Business Account balance is $14,250.60. You have $2,100.00 in pending transactions expected to clear by Monday.

Example 2: Write with Confirmation - Scheduling

Book a haircut for Sarah at 2pm tomorrow.
I've found Sarah Miller and confirmed your 2:00 PM slot is open tomorrow, Saturday, Feb 7th.

Action Summary:
Service: Haircut (60 mins)
Client: Sarah Miller
Time: 2:00 PM - 3:00 PM, Feb 7, 2026

Shall I add this to your calendar and send her a confirmation SMS?

Example 3: Disambiguation - CRM/Invoicing

Send an invoice to Sarah.
I found two clients named Sarah. Which one should I invoice?

1. Sarah Miller (smiller@email.com) - Last appointment: Yesterday.
2. Sarah Thompson (sthompson@design.co) - Last appointment: Jan 15th.

Example 4: Multi-step Workflow - Onboarding

Set up a new client with a contract and first invoice.
Great — I'll walk you through this. Let's start with the client.

Step 1 of 3: CLIENT
What's the client's name?
Marcus Reed, marcus@reedlogistics.com. The project is a "Warehouse Audit" for $5,000.
Done! I have:
1. Created a profile for Marcus Reed.
2. Generated a Standard Service Contract for the Warehouse Audit.
3. Prepared a draft invoice for $5,000.00.

Would you like to review the contract first, or shall I send the onboarding bundle to Marcus now?

Example 5: Error Recovery - Service Interruption

Create a new invoice for $200.
I'm sorry, I'm unable to reach the Invoicing service right now to create a new record. I can, however, take down the details as a Task and create it for you as soon as the service is back online. Would you like me to do that?

14. Context-Aware Greetings

Luma adjusts its greeting based on which Kasha app the user is viewing, offering proactive assistance.

Page Context Luma's Greeting Message
Invoicing Dashboard "Welcome back. You have 3 overdue invoices totaling $1,240. Would you like me to send automated reminders or check your recent payments?"
Client Profile (e.g., John Doe) "I'm looking at John Doe's history. He hasn't visited in 3 months—shall I send a 'We Miss You' discount code or check his last payment status?"
Booking Calendar "Your morning is fully booked, but you have a gap from 1:00 PM to 3:00 PM. Would you like me to open those slots for online booking or schedule your admin tasks?"
Banking Transactions "I see 12 unreconciled transactions from this week. Shall we categorize them now to keep your tax reports up to date?"
Website Builder "The 'Services' page was last updated 10 days ago. Would you like to add a new gallery block or optimize your SEO descriptions for better search ranking?"
Main Dashboard (Fresh Login) "Good morning! You have 4 appointments today and 2 new leads from your website. Where would you like to focus first?"

15. Quick Action Mapping

Context-aware quick action chips appear above the input field, adapting to the current app module.

App/UI Module Quick Action Chips
lr-banking-ui Transfer Funds View Statement Categorize Expenses Freeze Card
lr-invoicing-ui New Invoice Send Reminders View Tax Est. Record Payment
lr-booking-ui New Appt Block Time Share Booking Link Day Summary
lr-crm-ui Add Client Import Contacts Email List Note to Last Client
lr-website-builder-ui Publish Site Edit SEO Add Service View Analytics
lr-company-ui Update Hours Team Access Business Logo Subscription
lr-ecom-ui Add Product Inventory Check Discount Code Order Status
lr-kyc-ui Verify User Audit Log Verification Status Policy Update
lr-crm-ui (Comms) Send SMS Email Campaign Chat Settings Auto-Reply
lr-documents-ui Upload File Sign Request Contract Template Archive

16. UI Component Prototypes

Production-ready React TypeScript components with Tailwind CSS, designed for Next.js 15 and React 19.

LumaStreamingText

Core Component

Progressive text rendering with blinking cursor for streaming AI responses.

'use client';

import React, { useState, useEffect, useRef } from 'react';
import { cn } from '@kashatech/lr-fe-framework/lib/utils/cn.utils';

interface LumaStreamingTextProps {
  text: string;
  isStreaming?: boolean;
  speed?: number;
  className?: string;
  onComplete?: () => void;
}

export const LumaStreamingText: React.FC<LumaStreamingTextProps> = ({
  text,
  isStreaming = false,
  speed = 20,
  className,
  onComplete,
}) => {
  const [displayedText, setDisplayedText] = useState('');
  const [currentIndex, setCurrentIndex] = useState(0);
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);

  useEffect(() => {
    if (!isStreaming) {
      setDisplayedText(text);
      return;
    }

    if (currentIndex < text.length) {
      timeoutRef.current = setTimeout(() => {
        setDisplayedText((prev) => prev + text[currentIndex]);
        setCurrentIndex((prev) => prev + 1);
      }, speed);
    } else if (onComplete) {
      onComplete();
    }

    return () => {
      if (timeoutRef.current) clearTimeout(timeoutRef.current);
    };
  }, [text, currentIndex, isStreaming, speed, onComplete]);

  return (
    <div className={cn('relative inline-block font-sans text-sm leading-relaxed', className)}>
      {displayedText}
      {isStreaming && (
        <span className="inline-block w-1.5 h-4 ml-1 bg-purple-500 animate-pulse align-middle" />
      )}
    </div>
  );
};

LumaMessageBubble

Core Component

Message bubble handling both user and Luma messages with avatar, timestamp, and streaming support.

'use client';

import React from 'react';
import { cn } from '@kashatech/lr-fe-framework/lib/utils/cn.utils';
import { LumaStreamingText } from './LumaStreamingText';
import { Sparkles } from 'lucide-react';

interface LumaMessage {
  id: string;
  role: 'user' | 'assistant';
  content: string;
  timestamp: Date;
  isStreaming?: boolean;
}

interface LumaMessageBubbleProps {
  message: LumaMessage;
}

export const LumaMessageBubble: React.FC<LumaMessageBubbleProps> = ({ message }) => {
  const isAssistant = message.role === 'assistant';

  return (
    <div
      className={cn(
        'flex w-full mb-4 animate-in fade-in slide-in-from-bottom-2 duration-300',
        isAssistant ? 'justify-start' : 'justify-end'
      )}
    >
      <div className={cn('flex max-w-[85%] px-1', isAssistant ? 'flex-row' : 'flex-row-reverse')}>
        {isAssistant && (
          <div className="flex-shrink-0 w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center mr-2 mt-1 shadow-sm">
            <Sparkles className="w-4 h-4 text-white" />
          </div>
        )}

        <div
          className={cn(
            'relative px-4 py-2.5 rounded-2xl shadow-sm border',
            isAssistant
              ? 'bg-white text-gray-800 rounded-tl-none border-gray-100'
              : 'bg-purple-600 text-white rounded-tr-none border-purple-500'
          )}
        >
          {isAssistant ? (
            <LumaStreamingText
              text={message.content}
              isStreaming={message.isStreaming}
            />
          ) : (
            <p className="text-sm font-sans leading-relaxed">{message.content}</p>
          )}

          <div
            className={cn(
              'text-[10px] mt-1 opacity-50',
              isAssistant ? 'text-gray-500' : 'text-purple-100 text-right'
            )}
          >
            {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
          </div>
        </div>
      </div>
    </div>
  );
};

LumaComposer

Core Component

Auto-growing textarea input with send button and context-aware quick action chips.

'use client';

import React, { useState, useRef, useEffect } from 'react';
import { Send, Sparkles, PlusCircle } from 'lucide-react';
import { Button } from '@kashatech/lr-fe-framework/components/ui/button';
import { cn } from '@kashatech/lr-fe-framework/lib/utils/cn.utils';

interface QuickAction {
  id: string;
  label: string;
  icon?: React.ReactNode;
}

interface LumaComposerProps {
  onSendMessage: (text: string) => void;
  isLoading?: boolean;
  quickActions?: QuickAction[];
}

const DEFAULT_QUICK_ACTIONS: QuickAction[] = [
  { id: 'balance', label: 'Check balance' },
  { id: 'schedule', label: 'Today schedule' },
  { id: 'invoice', label: 'Send invoice' },
];

export const LumaComposer: React.FC<LumaComposerProps> = ({
  onSendMessage,
  isLoading = false,
  quickActions = DEFAULT_QUICK_ACTIONS,
}) => {
  const [input, setInput] = useState('');
  const textareaRef = useRef<HTMLTextAreaElement>(null);

  const adjustHeight = () => {
    const textarea = textareaRef.current;
    if (textarea) {
      textarea.style.height = 'auto';
      textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`;
    }
  };

  useEffect(() => {
    adjustHeight();
  }, [input]);

  const handleSubmit = (e?: React.FormEvent) => {
    e?.preventDefault();
    if (input.trim() && !isLoading) {
      onSendMessage(input.trim());
      setInput('');
    }
  };

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

  return (
    <div className="p-3 bg-white border-t border-gray-100">
      {/* Quick Action Chips */}
      <div className="flex gap-2 mb-3 overflow-x-auto no-scrollbar pb-1">
        {quickActions.map((action) => (
          <button
            key={action.id}
            onClick={() => onSendMessage(action.label)}
            className="flex-shrink-0 px-3 py-1.5 text-xs font-medium text-purple-700 bg-purple-50 rounded-full border border-purple-100 hover:bg-purple-100 transition-colors flex items-center gap-1"
          >
            {action.label}
          </button>
        ))}
      </div>

      <form onSubmit={handleSubmit} className="relative flex items-end gap-2 bg-gray-50 rounded-2xl p-2 border border-gray-200 focus-within:border-purple-300 transition-colors">
        <button type="button" className="p-2 text-gray-400 hover:text-purple-500 transition-colors">
          <PlusCircle className="w-5 h-5" />
        </button>

        <textarea
          ref={textareaRef}
          rows={1}
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={handleKeyDown}
          placeholder="Ask Luma anything..."
          className="w-full bg-transparent border-none focus:ring-0 text-sm py-2 resize-none max-h-[120px] text-gray-700 placeholder:text-gray-400"
        />

        <Button
          type="submit"
          disabled={!input.trim() || isLoading}
          size="icon"
          className={cn(
            "rounded-xl w-10 h-10 flex-shrink-0 transition-all duration-200",
            input.trim() ? "bg-purple-600 hover:bg-purple-700 shadow-md" : "bg-gray-200"
          )}
        >
          {isLoading ? (
            <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
          ) : (
            <Send className="w-4 h-4 text-white" />
          )}
        </Button>
      </form>
      <div className="text-[10px] text-center text-gray-400 mt-2 flex items-center justify-center gap-1">
        <Sparkles className="w-2.5 h-2.5" />
        Luma AI can make mistakes. Check important info.
      </div>
    </div>
  );
};

LumaChatPanel

Layout Component

Expanded 400x600px chat panel with header, message list, context bar, and composer.

'use client';

import React, { useRef, useEffect } from 'react';
import { X, Minus, Sparkles, Search, MoreHorizontal } from 'lucide-react';
import { LumaMessageBubble } from './LumaMessageBubble';
import { LumaComposer } from './LumaComposer';
import { cn } from '@kashatech/lr-fe-framework/lib/utils/cn.utils';

interface Message {
  id: string;
  role: 'user' | 'assistant';
  content: string;
  timestamp: Date;
  isStreaming?: boolean;
}

interface LumaChatPanelProps {
  isOpen: boolean;
  onClose: () => void;
  onMinimize: () => void;
}

export const LumaChatPanel: React.FC<LumaChatPanelProps> = ({ isOpen, onClose, onMinimize }) => {
  const [messages, setMessages] = React.useState<Message[]>([
    {
      id: '1',
      role: 'assistant',
      content: "Hi Jurgen! I noticed you have 3 overdue invoices. Would you like me to send reminders?",
      timestamp: new Date(),
    }
  ]);
  const [isTyping, setIsTyping] = React.useState(false);
  const scrollRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (scrollRef.current) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
    }
  }, [messages]);

  const handleSendMessage = (text: string) => {
    const userMsg: Message = {
      id: Date.now().toString(),
      role: 'user',
      content: text,
      timestamp: new Date(),
    };
    setMessages((prev) => [...prev, userMsg]);

    setIsTyping(true);
    setTimeout(() => {
      setIsTyping(false);
      const aiMsg: Message = {
        id: (Date.now() + 1).toString(),
        role: 'assistant',
        content: `I'm processing your request for "${text}". Just a moment...`,
        timestamp: new Date(),
        isStreaming: true,
      };
      setMessages((prev) => [...prev, aiMsg]);
    }, 1000);
  };

  if (!isOpen) return null;

  return (
    <div className="fixed bottom-24 right-6 w-[400px] h-[600px] bg-white rounded-3xl shadow-2xl flex flex-col overflow-hidden border border-gray-100 z-[9999] animate-in zoom-in-95 fade-in duration-300 origin-bottom-right">
      {/* Header */}
      <div className="bg-gradient-to-r from-purple-600 to-indigo-700 p-4 text-white">
        <div className="flex justify-between items-center mb-3">
          <div className="flex items-center gap-2">
            <div className="w-8 h-8 rounded-lg bg-white/20 flex items-center justify-center backdrop-blur-md">
              <Sparkles className="w-5 h-5 text-white" />
            </div>
            <div>
              <h3 className="font-semibold text-sm">Luma AI</h3>
              <div className="flex items-center gap-1.5">
                <div className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />
                <span className="text-[10px] text-purple-100">Personal Assistant • Kasha</span>
              </div>
            </div>
          </div>
          <div className="flex items-center gap-1">
            <button onClick={onMinimize} className="p-1.5 hover:bg-white/10 rounded-lg transition-colors">
              <Minus className="w-4 h-4" />
            </button>
            <button onClick={onClose} className="p-1.5 hover:bg-white/10 rounded-lg transition-colors">
              <X className="w-4 h-4" />
            </button>
          </div>
        </div>

        {/* Context Bar */}
        <div className="bg-black/10 rounded-xl p-2.5 flex items-center justify-between backdrop-blur-sm border border-white/10">
          <div className="flex items-center gap-2 overflow-hidden">
            <Search className="w-3.5 h-3.5 text-purple-200" />
            <span className="text-xs text-purple-50 truncate">Viewing "Invoicing Dashboard"</span>
          </div>
          <MoreHorizontal className="w-4 h-4 text-purple-200 cursor-pointer" />
        </div>
      </div>

      {/* Message List */}
      <div
        ref={scrollRef}
        className="flex-1 overflow-y-auto p-4 bg-gray-50/50 space-y-2 no-scrollbar"
      >
        <div className="text-center py-4">
          <span className="text-[10px] font-medium text-gray-400 uppercase tracking-wider">Today</span>
        </div>
        {messages.map((msg) => (
          <LumaMessageBubble key={msg.id} message={msg} />
        ))}
        {isTyping && (
          <div className="flex justify-start mb-4">
            <div className="flex items-center gap-2 bg-white px-4 py-3 rounded-2xl rounded-tl-none border border-gray-100 shadow-sm">
              <div className="flex gap-1">
                <div className="w-1.5 h-1.5 bg-purple-400 rounded-full animate-bounce [animation-delay:-0.3s]" />
                <div className="w-1.5 h-1.5 bg-purple-400 rounded-full animate-bounce [animation-delay:-0.15s]" />
                <div className="w-1.5 h-1.5 bg-purple-400 rounded-full animate-bounce" />
              </div>
            </div>
          </div>
        )}
      </div>

      {/* Composer */}
      <LumaComposer onSendMessage={handleSendMessage} isLoading={isTyping} />
    </div>
  );
};

LumaMinimizedButton

Layout Component

Floating 56x56px action button with unread badge, pulse animation, and suggestion tooltip.

'use client';

import React from 'react';
import { Sparkles } from 'lucide-react';
import { cn } from '@kashatech/lr-fe-framework/lib/utils/cn.utils';

interface LumaMinimizedButtonProps {
  onClick: () => void;
  unreadCount?: number;
  hasSuggestion?: boolean;
}

export const LumaMinimizedButton: React.FC<LumaMinimizedButtonProps> = ({
  onClick,
  unreadCount = 0,
  hasSuggestion = false,
}) => {
  return (
    <div className="fixed bottom-6 right-6 z-[9999]">
      <button
        onClick={onClick}
        className={cn(
          "relative w-14 h-14 rounded-full flex items-center justify-center shadow-xl transition-all duration-300 hover:scale-110 active:scale-95 group overflow-hidden",
          hasSuggestion
            ? "bg-gradient-to-br from-purple-600 via-indigo-600 to-pink-500 animate-gradient-x"
            : "bg-purple-600 hover:bg-purple-700"
        )}
      >
        {hasSuggestion && (
          <span className="absolute inset-0 rounded-full bg-purple-400 animate-ping opacity-25 scale-150" />
        )}

        <Sparkles className="w-7 h-7 text-white group-hover:rotate-12 transition-transform duration-300" />

        {unreadCount > 0 && (
          <span className="absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-pink-500 text-[10px] font-bold text-white border-2 border-white shadow-sm animate-in zoom-in">
            {unreadCount > 9 ? '9+' : unreadCount}
          </span>
        )}
      </button>

      {hasSuggestion && (
        <div className="absolute right-16 bottom-2 bg-white px-3 py-2 rounded-xl shadow-lg border border-purple-100 whitespace-nowrap animate-in slide-in-from-right-2 fade-in duration-500">
          <div className="text-xs font-semibold text-purple-900">Luma has a suggestion</div>
          <div className="text-[10px] text-gray-500">About your overdue invoices</div>
          <div className="absolute top-1/2 -right-1.5 -translate-y-1/2 border-y-[6px] border-y-transparent border-l-[6px] border-l-white" />
        </div>
      )}
    </div>
  );
};

LumaWidget

Orchestrator

Top-level orchestrator coordinating minimized button and expanded panel states.

'use client';

import React, { useState } from 'react';
import { LumaMinimizedButton } from './LumaMinimizedButton';
import { LumaChatPanel } from './LumaChatPanel';

export const LumaWidget: React.FC = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [unreadCount, setUnreadCount] = useState(1);
  const [hasSuggestion, setHasSuggestion] = useState(true);

  const handleOpen = () => {
    setIsOpen(true);
    setUnreadCount(0);
    setHasSuggestion(false);
  };

  const handleClose = () => {
    setIsOpen(false);
  };

  const handleMinimize = () => {
    setIsOpen(false);
  };

  return (
    <>
      {!isOpen && (
        <LumaMinimizedButton
          onClick={handleOpen}
          unreadCount={unreadCount}
          hasSuggestion={hasSuggestion}
        />
      )}
      <LumaChatPanel
        isOpen={isOpen}
        onClose={handleClose}
        onMinimize={handleMinimize}
      />
    </>
  );
};

17. Rich Response Cards

Interactive cards for confirmations, disambiguations, appointments, invoices, balances, progress tracking, and error recovery.

ConfirmationCard

Response Card

Risk-tiered confirmation interface with color-coded severity levels (low, medium, high, critical).

'use client';

import React, { useState } from 'react';
import { AlertTriangle, ShieldAlert, CheckCircle2, X } from 'lucide-react';

interface ConfirmationCardProps {
  title: string;
  description: string;
  severity: 'low' | 'medium' | 'high' | 'critical';
  details: Record<string, string>;
  onConfirm: () => void;
  onCancel: () => void;
}

export const ConfirmationCard: React.FC<ConfirmationCardProps> = ({
  title, description, severity, details, onConfirm, onCancel,
}) => {
  const [confirmText, setConfirmText] = useState('');

  const styles = {
    low: { bg: 'bg-slate-50', border: 'border-slate-200', text: 'text-slate-900', icon: <CheckCircle2 className="text-slate-500" />, btn: 'bg-slate-900 hover:bg-slate-800' },
    medium: { bg: 'bg-blue-50', border: 'border-blue-200', text: 'text-blue-900', icon: <CheckCircle2 className="text-blue-500" />, btn: 'bg-blue-600 hover:bg-blue-700' },
    high: { bg: 'bg-amber-50', border: 'border-amber-200', text: 'text-amber-900', icon: <AlertTriangle className="text-amber-500" />, btn: 'bg-amber-600 hover:bg-amber-700' },
    critical: { bg: 'bg-red-50', border: 'border-red-200', text: 'text-red-900', icon: <ShieldAlert className="text-red-500" />, btn: 'bg-red-600 hover:bg-red-700 disabled:bg-red-300' },
  }[severity];

  return (
    <div className={`my-4 p-5 rounded-2xl border ${styles.border} ${styles.bg} animate-in fade-in slide-in-from-bottom-2 duration-500 shadow-sm max-w-md`}>
      <div className="flex items-start gap-3 mb-4">
        {styles.icon}
        <div>
          <h3 className={`font-semibold ${styles.text}`}>{title}</h3>
          <p className="text-sm opacity-80 mt-1">{description}</p>
        </div>
      </div>

      <div className="space-y-2 mb-5">
        {Object.entries(details).map(([key, value]) => (
          <div key={key} className="flex justify-between text-xs py-1.5 border-b border-black/5 last:border-0">
            <span className="font-medium uppercase tracking-wider opacity-60">{key}</span>
            <span className="font-semibold">{value}</span>
          </div>
        ))}
      </div>

      {severity === 'critical' && (
        <div className="mb-4">
          <label className="text-[10px] font-bold uppercase text-red-600 mb-1 block">Type 'CONFIRM' to proceed</label>
          <input
            type="text"
            className="w-full px-3 py-2 rounded-lg border border-red-200 focus:outline-none focus:ring-2 focus:ring-red-500 text-sm"
            placeholder="CONFIRM"
            value={confirmText}
            onChange={(e) => setConfirmText(e.target.value)}
          />
        </div>
      )}

      <div className="flex gap-2">
        <button
          onClick={onCancel}
          className="flex-1 px-4 py-2 rounded-xl text-sm font-medium border border-black/10 hover:bg-black/5 transition-colors"
        >
          Cancel
        </button>
        <button
          onClick={onConfirm}
          disabled={severity === 'critical' && confirmText !== 'CONFIRM'}
          className={`flex-1 px-4 py-2 rounded-xl text-sm font-medium text-white transition-all ${styles.btn}`}
        >
          Confirm
        </button>
      </div>
    </div>
  );
};

DisambiguationCard

Response Card

Numbered clickable options for resolving ambiguous entities or multi-match scenarios.

'use client';

import React from 'react';
import { ChevronRight, HelpCircle } from 'lucide-react';

interface DisambiguationCardProps {
  question: string;
  options: Array<{ label: string; value: string; description?: string }>;
  onSelect: (value: string) => void;
}

export const DisambiguationCard: React.FC<DisambiguationCardProps> = ({
  question, options, onSelect,
}) => {
  return (
    <div className="my-4 overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm max-w-sm">
      <div className="bg-gray-50 px-4 py-3 border-b border-gray-100 flex items-center gap-2">
        <HelpCircle size={16} className="text-blue-500" />
        <p className="text-sm font-medium text-gray-700">{question}</p>
      </div>
      <div className="divide-y divide-gray-100">
        {options.map((option, index) => (
          <button
            key={option.value}
            onClick={() => onSelect(option.value)}
            className="w-full text-left px-4 py-3 hover:bg-blue-50 transition-colors flex items-center justify-between group"
          >
            <div className="flex gap-3 items-center">
              <span className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 text-[10px] font-bold text-gray-500 group-hover:bg-blue-500 group-hover:text-white transition-colors">
                {index + 1}
              </span>
              <div>
                <div className="text-sm font-bold text-gray-900">{option.label}</div>
                {option.description && (
                  <div className="text-xs text-gray-500 leading-tight mt-0.5">{option.description}</div>
                )}
              </div>
            </div>
            <ChevronRight size={16} className="text-gray-300 group-hover:text-blue-400 transition-colors" />
          </button>
        ))}
      </div>
    </div>
  );
};

AppointmentCard

Response Card

Displays appointment details with status badge and action buttons (Confirm, Reschedule, Cancel).

'use client';

import React from 'react';
import { Calendar, Clock, MapPin, User, CheckCircle, AlertCircle, XCircle } from 'lucide-react';

interface AppointmentCardProps {
  date: string; time: string; duration: string; clientName: string;
  serviceName: string; locationName: string; staffName?: string;
  status: 'pending' | 'confirmed' | 'cancelled';
  onAction?: (action: 'confirm' | 'reschedule' | 'cancel') => void;
}

export const AppointmentCard: React.FC<AppointmentCardProps> = ({
  date, time, duration, clientName, serviceName, locationName, staffName, status, onAction
}) => {
  const statusConfig = {
    confirmed: { color: 'text-emerald-600', bg: 'bg-emerald-50', border: 'border-emerald-100', icon: <CheckCircle size={14} /> },
    pending: { color: 'text-amber-600', bg: 'bg-amber-50', border: 'border-amber-100', icon: <AlertCircle size={14} /> },
    cancelled: { color: 'text-red-600', bg: 'bg-red-50', border: 'border-red-100', icon: <XCircle size={14} /> },
  }[status];

  return (
    <div className="my-4 rounded-2xl border border-gray-200 bg-white overflow-hidden shadow-sm max-w-sm">
      <div className={`px-4 py-2 border-b flex items-center justify-between ${statusConfig.bg} ${statusConfig.border}`}>
        <span className={`text-[10px] font-bold uppercase tracking-widest flex items-center gap-1.5 ${statusConfig.color}`}>
          {statusConfig.icon} {status}
        </span>
        <span className="text-[10px] font-medium text-gray-400 uppercase tracking-widest">ID: {Math.random().toString(36).substr(2, 6).toUpperCase()}</span>
      </div>

      <div className="p-4 space-y-4">
        <div>
          <h4 className="text-lg font-bold text-gray-900 leading-tight">{serviceName}</h4>
          <p className="text-sm text-gray-500 flex items-center gap-1 mt-1">
            <User size={14} /> {clientName}
          </p>
        </div>

        <div className="grid grid-cols-2 gap-4 py-3 border-y border-gray-50">
          <div className="space-y-1">
            <div className="flex items-center gap-2 text-gray-400 text-[10px] font-bold uppercase tracking-wider">
              <Calendar size={12} /> Date
            </div>
            <div className="text-sm font-semibold">{date}</div>
          </div>
          <div className="space-y-1">
            <div className="flex items-center gap-2 text-gray-400 text-[10px] font-bold uppercase tracking-wider">
              <Clock size={12} /> Time
            </div>
            <div className="text-sm font-semibold">{time} <span className="text-gray-400 font-normal">({duration})</span></div>
          </div>
        </div>

        <div className="space-y-2 text-sm text-gray-600">
          <div className="flex items-start gap-2">
            <MapPin size={16} className="text-gray-400 shrink-0 mt-0.5" />
            <span>{locationName}</span>
          </div>
          {staffName && (
            <div className="flex items-center gap-2">
              <div className="w-4 h-4 rounded-full bg-blue-100 text-[8px] flex items-center justify-center font-bold text-blue-600 uppercase">
                {staffName[0]}
              </div>
              <span>Assigned to <span className="font-medium text-gray-900">{staffName}</span></span>
            </div>
          )}
        </div>

        <div className="flex gap-2 pt-2">
          {status === 'pending' && (
            <button onClick={() => onAction?.('confirm')} className="flex-1 bg-emerald-600 text-white text-xs font-bold py-2.5 rounded-xl hover:bg-emerald-700 transition-colors shadow-sm shadow-emerald-200">
              Confirm
            </button>
          )}
          <button onClick={() => onAction?.('reschedule')} className="flex-1 bg-gray-50 text-gray-700 text-xs font-bold py-2.5 rounded-xl hover:bg-gray-100 transition-colors border border-gray-200">
            Reschedule
          </button>
          <button onClick={() => onAction?.('cancel')} className="flex-none px-3 bg-white text-red-500 text-xs font-bold py-2.5 rounded-xl hover:bg-red-50 transition-colors border border-red-100">
            Cancel
          </button>
        </div>
      </div>
    </div>
  );
};

InvoiceCard

Response Card

Invoice preview with line items, VAT calculation, totals, and action buttons (Create & Send, Edit, Cancel).

'use client';

import React from 'react';
import { FileText, Download, Send, Edit3, Trash2 } from 'lucide-react';

interface InvoiceCardProps {
  invoiceNumber: string; clientName: string;
  lineItems: Array<{ description: string; quantity: number; unitPrice: number }>;
  subtotal: number; vatAmount: number; vatRate: number; total: number;
  currency: string; dueDate: string;
  status: 'draft' | 'sent' | 'paid' | 'overdue';
  onAction?: (action: string) => void;
}

export const InvoiceCard: React.FC<InvoiceCardProps> = ({
  invoiceNumber, clientName, lineItems, subtotal, vatAmount, vatRate, total, currency, dueDate, status, onAction
}) => {
  const statusMap = {
    paid: 'bg-green-100 text-green-700',
    sent: 'bg-blue-100 text-blue-700',
    draft: 'bg-gray-100 text-gray-600',
    overdue: 'bg-red-100 text-red-700',
  };

  return (
    <div className="my-4 p-6 rounded-2xl border border-gray-200 bg-white shadow-lg max-w-md">
      <div className="flex justify-between items-start mb-6">
        <div>
          <div className="flex items-center gap-2 mb-1">
            <FileText className="text-blue-600" size={20} />
            <h3 className="text-lg font-bold text-gray-900">Invoice {invoiceNumber}</h3>
          </div>
          <p className="text-sm text-gray-500">Bill to: <span className="font-semibold text-gray-700">{clientName}</span></p>
        </div>
        <span className={`text-[10px] font-bold uppercase tracking-wider px-2 py-1 rounded-md ${statusMap[status]}`}>
          {status}
        </span>
      </div>

      <div className="overflow-hidden border-t border-gray-100 mb-4">
        <table className="w-full text-left text-sm">
          <thead>
            <tr className="text-[10px] font-bold text-gray-400 uppercase tracking-widest border-b border-gray-50">
              <th className="py-3 font-medium">Item</th>
              <th className="py-3 text-right font-medium">Qty</th>
              <th className="py-3 text-right font-medium">Price</th>
            </tr>
          </thead>
          <tbody className="divide-y divide-gray-50">
            {lineItems.map((item, i) => (
              <tr key={i}>
                <td className="py-3 text-gray-700">{item.description}</td>
                <td className="py-3 text-right text-gray-500">{item.quantity}</td>
                <td className="py-3 text-right text-gray-700 font-medium">
                  {currency}{item.unitPrice.toLocaleString()}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <div className="bg-gray-50 rounded-xl p-4 space-y-2 mb-6">
        <div className="flex justify-between text-xs text-gray-500">
          <span>Subtotal</span>
          <span>{currency}{subtotal.toLocaleString()}</span>
        </div>
        <div className="flex justify-between text-xs text-gray-500">
          <span>VAT ({vatRate}%)</span>
          <span>{currency}{vatAmount.toLocaleString()}</span>
        </div>
        <div className="flex justify-between text-base font-bold text-gray-900 pt-2 border-t border-gray-200">
          <span>Total</span>
          <span>{currency}{total.toLocaleString()}</span>
        </div>
        <div className="text-[10px] text-gray-400 text-center pt-2 italic">
          Due by {dueDate}
        </div>
      </div>

      <div className="flex gap-2">
        {status === 'draft' && (
          <button onClick={() => onAction?.('send')} className="flex-1 bg-blue-600 text-white text-xs font-bold py-2.5 rounded-xl flex items-center justify-center gap-2 hover:bg-blue-700 transition-colors shadow-sm shadow-blue-200">
            <Send size={14} /> Create & Send
          </button>
        )}
        <button onClick={() => onAction?.('edit')} className="flex-1 bg-white text-gray-700 text-xs font-bold py-2.5 rounded-xl flex items-center justify-center gap-2 border border-gray-200 hover:bg-gray-50 transition-colors">
          <Edit3 size={14} /> Edit
        </button>
        <button onClick={() => onAction?.('cancel')} className="p-2.5 rounded-xl border border-red-100 text-red-500 hover:bg-red-50 transition-colors">
          <Trash2 size={16} />
        </button>
      </div>
    </div>
  );
};

BalanceCard

Response Card

Account balance display with trend indicator and recent transaction list.

'use client';

import React from 'react';
import { TrendingUp, TrendingDown, ArrowUpRight, Wallet, History } from 'lucide-react';

interface BalanceCardProps {
  balance: number; currency: string; accountName: string;
  recentTransactions: Array<{ amount: number; description: string; date: string; direction: 'in' | 'out' }>;
}

export const BalanceCard: React.FC<BalanceCardProps> = ({
  balance, currency, accountName, recentTransactions
}) => {
  return (
    <div className="my-4 rounded-2xl border border-gray-200 bg-white overflow-hidden shadow-sm max-w-sm">
      <div className="p-6 bg-gradient-to-br from-slate-900 to-slate-800 text-white">
        <div className="flex justify-between items-center mb-4">
          <div className="p-2 bg-white/10 rounded-lg">
            <Wallet size={18} className="text-slate-300" />
          </div>
          <span className="text-[10px] font-bold uppercase tracking-widest text-slate-400">{accountName}</span>
        </div>
        <p className="text-slate-400 text-sm mb-1">Available Balance</p>
        <div className="flex items-baseline gap-2">
          <h2 className="text-3xl font-bold">{currency}{balance.toLocaleString()}</h2>
          <span className={`flex items-center text-xs font-bold ${balance >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
            {balance >= 0 ? <TrendingUp size={12} className="mr-1" /> : <TrendingDown size={12} className="mr-1" />}
            +2.4%
          </span>
        </div>
      </div>

      <div className="p-4">
        <div className="flex justify-between items-center mb-3">
          <h4 className="text-[10px] font-bold uppercase tracking-widest text-gray-400 flex items-center gap-1.5">
            <History size={12} /> Recent Transactions
          </h4>
        </div>
        <div className="space-y-3">
          {recentTransactions.map((tx, i) => (
            <div key={i} className="flex justify-between items-center">
              <div className="flex gap-3 items-center">
                <div className={`p-1.5 rounded-lg ${tx.direction === 'in' ? 'bg-emerald-50 text-emerald-600' : 'bg-rose-50 text-rose-600'}`}>
                  {tx.direction === 'in' ? <ArrowUpRight size={14} className="rotate-180" /> : <ArrowUpRight size={14} />}
                </div>
                <div>
                  <p className="text-xs font-bold text-gray-900 leading-none mb-1">{tx.description}</p>
                  <p className="text-[10px] text-gray-400">{tx.date}</p>
                </div>
              </div>
              <span className={`text-sm font-bold ${tx.direction === 'in' ? 'text-emerald-600' : 'text-gray-900'}`}>
                {tx.direction === 'in' ? '+' : '-'}{currency}{tx.amount.toLocaleString()}
              </span>
            </div>
          ))}
        </div>
        <button className="w-full mt-5 py-2 text-xs font-bold text-blue-600 bg-blue-50 hover:bg-blue-100 transition-colors rounded-xl">
          View all transactions
        </button>
      </div>
    </div>
  );
};

ProgressCard

Response Card

Multi-step workflow progress tracker with status indicators (pending, active, complete).

'use client';

import React from 'react';
import { Check, Loader2, Circle } from 'lucide-react';

interface ProgressCardProps {
  title: string;
  steps: Array<{ label: string; status: 'pending' | 'active' | 'complete' }>;
  currentStep: number;
}

export const ProgressCard: React.FC<ProgressCardProps> = ({ title, steps, currentStep }) => {
  return (
    <div className="my-4 p-5 rounded-2xl border border-gray-200 bg-white shadow-sm max-w-sm">
      <h3 className="text-sm font-bold text-gray-900 mb-6 px-1 flex items-center gap-2">
        <span className="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse" />
        {title}
      </h3>

      <div className="space-y-0">
        {steps.map((step, index) => {
          const isLast = index === steps.length - 1;
          const status = step.status;

          return (
            <div key={index} className="flex gap-4">
              <div className="flex flex-col items-center">
                <div className={`
                  z-10 w-6 h-6 rounded-full flex items-center justify-center transition-all duration-300
                  ${status === 'complete' ? 'bg-emerald-500 text-white' :
                    status === 'active' ? 'bg-blue-100 text-blue-600 border-2 border-blue-500 ring-4 ring-blue-50' :
                    'bg-gray-100 text-gray-400 border border-gray-200'}
                `}>
                  {status === 'complete' ? <Check size={14} strokeWidth={3} /> :
                   status === 'active' ? <Loader2 size={12} className="animate-spin" /> :
                   <Circle size={8} fill="currentColor" />}
                </div>
                {!isLast && (
                  <div className={`w-0.5 h-10 transition-colors duration-500 ${status === 'complete' ? 'bg-emerald-500' : 'bg-gray-100'}`} />
                )}
              </div>
              <div className="pt-0.5 pb-6">
                <p className={`text-sm font-bold transition-colors ${status === 'active' ? 'text-blue-600' : status === 'complete' ? 'text-gray-900' : 'text-gray-400'}`}>
                  {step.label}
                </p>
                {status === 'active' && (
                  <span className="text-[10px] text-blue-500 font-medium animate-pulse">Processing...</span>
                )}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
};

ErrorCard

Response Card

Error display with recovery action suggestions and dismiss option.

'use client';

import React from 'react';
import { AlertTriangle, RefreshCw, XCircle, ChevronRight } from 'lucide-react';

interface ErrorCardProps {
  title: string;
  message: string;
  recoveryOptions: Array<{ label: string; action: () => void }>;
}

export const ErrorCard: React.FC<ErrorCardProps> = ({ title, message, recoveryOptions }) => {
  return (
    <div className="my-4 overflow-hidden rounded-2xl border border-red-200 bg-white shadow-lg shadow-red-100/50 max-w-sm animate-in zoom-in-95 duration-200">
      <div className="bg-red-50 p-4 flex items-center gap-3">
        <div className="p-2 bg-red-100 rounded-xl text-red-600">
          <AlertTriangle size={20} />
        </div>
        <div>
          <h3 className="text-sm font-bold text-red-900">{title}</h3>
          <p className="text-[11px] text-red-700/80 font-medium">ERROR_CODE: {Math.random().toString(36).substr(7).toUpperCase()}</p>
        </div>
      </div>

      <div className="p-4">
        <p className="text-sm text-gray-600 leading-relaxed mb-5">
          {message}
        </p>

        <div className="space-y-2">
          <p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-1">Recommended actions</p>
          {recoveryOptions.map((option, i) => (
            <button
              key={i}
              onClick={option.action}
              className="w-full flex items-center justify-between p-3 rounded-xl bg-gray-50 border border-gray-100 hover:bg-white hover:border-red-200 hover:shadow-sm transition-all group text-left"
            >
              <div className="flex items-center gap-2">
                <RefreshCw size={14} className="text-red-500 group-hover:rotate-180 transition-transform duration-500" />
                <span className="text-sm font-semibold text-gray-700">{option.label}</span>
              </div>
              <ChevronRight size={14} className="text-gray-300 group-hover:text-red-400 transition-transform group-hover:translate-x-0.5" />
            </button>
          ))}
          <button className="w-full p-3 rounded-xl flex items-center justify-center gap-2 text-xs font-bold text-gray-400 hover:text-gray-600 transition-colors">
            <XCircle size={14} /> Dismiss
          </button>
        </div>
      </div>
    </div>
  );
};