App Icon

Invoice Reader

AI Invoice Extraction Tool



MIT LicenseGitHub
ScreenshotScreenshot

Customization

Important: Before customizing this agent, you must first deploy it using the Deploy button above. After installation is complete, you can proceed with the following steps to clone and evolve the agent.
1. Install OpenKBS CLI:
npm install -g openkbs
2. Create and enter project directory:
mkdir my-agent && cd my-agent
3. Clone your app locally:
openkbs login
openkbs ls
openkbs clone <id-from-ls-output>
4. Initialize Git repository:
git init && git stage . && git commit -m "First commit"
5. Add new features using claude code ( npm install -g @anthropic-ai/claude-code ):
Examples:
claude "Add barcode item field to this invoice processing agent"
6. Review changes and deploy to OpenKBS:
git diff
openkbs push

Disclaimer

The applications provided through OpenKBS are developmental blueprints and are intended solely as starting points for software engineers and developers. These open-source templates are not production-ready solutions.

Before any production deployment, developers must:

  • Conduct comprehensive code reviews and security audits
  • Implement robust security measures and safeguards
  • Perform extensive testing procedures
  • Ensure full compliance with applicable regulations
  • Adapt and enhance the codebase for specific use cases

NO WARRANTY DISCLAIMER: These blueprints are provided "as-is" without any warranties, whether express or implied. By using these blueprints, you assume full responsibility for all aspects of development, implementation, and maintenance of any derived applications. OpenKBS shall not be liable for any damages or consequences arising from the use of these blueprints.

Instructions and Source Code

You are a professional invoice extraction tool.
Read this invoice and return valid JSON response.
Make sure you escape all double quotes in string values.
Extract all items from the invoice.


OUTPUT_JSON_RESPONSE:
{
  "invoice": {
    "number": "",
    "date": "",
    "place": "",
    "seller": {
      "name": "",
      "address": "",
      "TIN": "",
      "VAT": "",
      "representative": ""
    },
    "buyer": {
      "name": "",
      "address": "",
      "TIN": "",
      "VAT": "",
      "contact": "",
      "client_number": ""
    },
    "items": [
      {
        "no": 1,
        "description": "",
        "unit": "",
        "unit_price_without_vat":,
        "unit_price_with_vat": ,
        "quantity": ,
        "total_without_vat": ,
        "total_with_vat": 
      }
    ],
    "summary": {
      "base_total": ,
      "vat_rate": "",
      "vat_amount": ,
      "total": ,
      "prepaid_voucher": ,
      "amount_due": ,
      "currency": ""
    },
    "payment": {
      "type": "",
      "bank": "",
      "IBAN": "",
      "BIC": ""
    },
    "footnote": "."
  }
}

Events Files (Node.js Backend)

Events/actions.js

export const getActions = (meta) => [
  // New action to handle invoice save requests
  [/\{"type":"SAVE_INVOICE_REQUEST"[\s\S]*\}/, async (match) => {
    try {
      // Parse the JSON content
      const requestData = JSON.parse(match[0]);
      
      // Simulate API call to store the invoice
      // In a real app, this would make an actual API call
      console.log("Saving invoice data:", requestData);
      
      // Demo API call simulation - would be replaced with actual implementation
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      // Return success response
      return {
        type: "SAVE_INVOICE_SUCCESS",
        message: "Invoice has been successfully saved",
        timestamp: new Date().toISOString(),
        invoiceId: "INV-" + Math.floor(Math.random() * 10000),
        ...meta
      };
    } catch (e) {
      console.error("Error saving invoice:", e);
      
      // Return error response
      return {
        type: "SAVE_INVOICE_FAILED",
        error: e.message || "Failed to save invoice",
        timestamp: new Date().toISOString(),
        ...meta
      };
    }
  }]
];

Events/onRequest.js

import {getActions} from './actions.js';

export const handler = async (event) => {
    const actions = getActions({});

    for (let [regex, action] of actions) {
        const lastMessage = event.payload.messages[event.payload.messages.length - 1].content;
        const match = lastMessage?.match(regex);
        if (match) return await action(match);
    }

    return { type: 'CONTINUE' }
};

Events/onResponse.js

import {getActions} from './actions.js';

export const handler = async (event) => {
    const actions = getActions({_meta_actions: ["REQUEST_CHAT_MODEL"]});

    for (let [regex, action] of actions) {
        const lastMessage = event.payload.messages[event.payload.messages.length - 1].content;        
        const match = lastMessage?.match(regex);        
        if (match) return await action(match);            
    }

    return { type: 'CONTINUE' }
};

Frontend Files (React Frontend)

Frontend/InvoiceEditor.js

import React, { useState } from "react";
import { 
  Box, 
  Card, 
  CardContent, 
  Grid, 
  TextField, 
  Typography, 
  Button,
  Accordion,
  AccordionSummary,
  AccordionDetails,
  Divider
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import GetAppIcon from "@mui/icons-material/GetApp";
import { InvoiceItemsTable } from "./InvoiceItemsTable";

const isMobile = window.openkbs.isMobile;

export const InvoiceEditor = ({ invoiceData, onSave }) => {
  const [invoice, setInvoice] = useState(invoiceData.invoice || {});

  const handleBasicInfoChange = (e) => {
    const { name, value } = e.target;
    setInvoice({
      ...invoice,
      [name]: value
    });
  };

  const handleEntityInfoChange = (entity, field, value) => {
    setInvoice({
      ...invoice,
      [entity]: {
        ...invoice[entity],
        [field]: value
      }
    });
  };

  const handleSummaryChange = (field, value) => {
    setInvoice({
      ...invoice,
      summary: {
        ...invoice.summary,
        [field]: value
      }
    });
  };

  const handlePaymentChange = (field, value) => {
    setInvoice({
      ...invoice,
      payment: {
        ...invoice.payment,
        [field]: value
      }
    });
  };

  const handleItemsChange = (newItems) => {
    setInvoice({
      ...invoice,
      items: newItems
    });
  };

  const handleSaveClick = () => {
    onSave({
      invoice: invoice
    });
  };
  
  const handleDownloadClick = () => {
    // Create a JSON blob from the invoice data
    const invoiceJSON = JSON.stringify({ invoice }, null, 2);
    const blob = new Blob([invoiceJSON], { type: "application/json" });
    
    // Create a download link and trigger the download
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = `invoice-${invoice.number || "download"}.json`;
    document.body.appendChild(link);
    link.click();
    
    // Clean up
    URL.revokeObjectURL(url);
    document.body.removeChild(link);
  };

  return (
    <Box sx={{ width: "100%", mt: 2 }}>
      <Typography variant="h5" gutterBottom>
        Invoice Editor
      </Typography>

      <Card sx={{ mb: 3 }}>
        <CardContent>
          <Grid container spacing={2}>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="Invoice Number"
                name="number"
                value={invoice.number || ""}
                onChange={handleBasicInfoChange}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="Date"
                name="date"
                value={invoice.date || ""}
                onChange={handleBasicInfoChange}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="Place"
                name="place"
                value={invoice.place || ""}
                onChange={handleBasicInfoChange}
                variant="outlined"
                size="small"
              />
            </Grid>
          </Grid>
        </CardContent>
      </Card>

      <Accordion defaultExpanded>
        <AccordionSummary expandIcon={<ExpandMoreIcon />}>
          <Typography variant="h6">Seller Information</Typography>
        </AccordionSummary>
        <AccordionDetails>
          <Grid container spacing={2}>
            <Grid item xs={12} sm={6}>
              <TextField
                fullWidth
                label="Name"
                value={invoice.seller?.name || ""}
                onChange={(e) => handleEntityInfoChange("seller", "name", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={6}>
              <TextField
                fullWidth
                label="Address"
                value={invoice.seller?.address || ""}
                onChange={(e) => handleEntityInfoChange("seller", "address", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="TIN"
                value={invoice.seller?.TIN || ""}
                onChange={(e) => handleEntityInfoChange("seller", "TIN", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="VAT"
                value={invoice.seller?.VAT || ""}
                onChange={(e) => handleEntityInfoChange("seller", "VAT", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="Representative"
                value={invoice.seller?.representative || ""}
                onChange={(e) => handleEntityInfoChange("seller", "representative", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
          </Grid>
        </AccordionDetails>
      </Accordion>

      <Accordion>
        <AccordionSummary expandIcon={<ExpandMoreIcon />}>
          <Typography variant="h6">Buyer Information</Typography>
        </AccordionSummary>
        <AccordionDetails>
          <Grid container spacing={2}>
            <Grid item xs={12} sm={6}>
              <TextField
                fullWidth
                label="Name"
                value={invoice.buyer?.name || ""}
                onChange={(e) => handleEntityInfoChange("buyer", "name", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={6}>
              <TextField
                fullWidth
                label="Address"
                value={invoice.buyer?.address || ""}
                onChange={(e) => handleEntityInfoChange("buyer", "address", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="TIN"
                value={invoice.buyer?.TIN || ""}
                onChange={(e) => handleEntityInfoChange("buyer", "TIN", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="VAT"
                value={invoice.buyer?.VAT || ""}
                onChange={(e) => handleEntityInfoChange("buyer", "VAT", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="Contact"
                value={invoice.buyer?.contact || ""}
                onChange={(e) => handleEntityInfoChange("buyer", "contact", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="Client Number"
                value={invoice.buyer?.client_number || ""}
                onChange={(e) => handleEntityInfoChange("buyer", "client_number", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
          </Grid>
        </AccordionDetails>
      </Accordion>

      <Accordion defaultExpanded>
        <AccordionSummary expandIcon={<ExpandMoreIcon />}>
          <Typography variant="h6">Invoice Items</Typography>
        </AccordionSummary>
        <AccordionDetails>
          <InvoiceItemsTable items={invoice.items || []} onItemsChange={handleItemsChange} />
        </AccordionDetails>
      </Accordion>

      <Accordion>
        <AccordionSummary expandIcon={<ExpandMoreIcon />}>
          <Typography variant="h6">Summary</Typography>
        </AccordionSummary>
        <AccordionDetails>
          <Grid container spacing={2}>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="Base Total"
                value={invoice.summary?.base_total || ""}
                onChange={(e) => handleSummaryChange("base_total", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="VAT Rate"
                value={invoice.summary?.vat_rate || ""}
                onChange={(e) => handleSummaryChange("vat_rate", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="VAT Amount"
                value={invoice.summary?.vat_amount || ""}
                onChange={(e) => handleSummaryChange("vat_amount", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="Total"
                value={invoice.summary?.total || ""}
                onChange={(e) => handleSummaryChange("total", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="Prepaid Voucher"
                value={invoice.summary?.prepaid_voucher || ""}
                onChange={(e) => handleSummaryChange("prepaid_voucher", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="Amount Due"
                value={invoice.summary?.amount_due || ""}
                onChange={(e) => handleSummaryChange("amount_due", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={4}>
              <TextField
                fullWidth
                label="Currency"
                value={invoice.summary?.currency || ""}
                onChange={(e) => handleSummaryChange("currency", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
          </Grid>
        </AccordionDetails>
      </Accordion>

      <Accordion>
        <AccordionSummary expandIcon={<ExpandMoreIcon />}>
          <Typography variant="h6">Payment Information</Typography>
        </AccordionSummary>
        <AccordionDetails>
          <Grid container spacing={2}>
            <Grid item xs={12} sm={3}>
              <TextField
                fullWidth
                label="Payment Type"
                value={invoice.payment?.type || ""}
                onChange={(e) => handlePaymentChange("type", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={3}>
              <TextField
                fullWidth
                label="Bank"
                value={invoice.payment?.bank || ""}
                onChange={(e) => handlePaymentChange("bank", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={3}>
              <TextField
                fullWidth
                label="IBAN"
                value={invoice.payment?.IBAN || ""}
                onChange={(e) => handlePaymentChange("IBAN", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
            <Grid item xs={12} sm={3}>
              <TextField
                fullWidth
                label="BIC"
                value={invoice.payment?.BIC || ""}
                onChange={(e) => handlePaymentChange("BIC", e.target.value)}
                variant="outlined"
                size="small"
              />
            </Grid>
          </Grid>
        </AccordionDetails>
      </Accordion>

      <Box sx={{ mt: 3, mb: 2, display: "flex", justifyContent: "flex-end", gap: 2 }}>
        <Button
          variant="contained"
          color="secondary"
          onClick={handleDownloadClick}
        >
          Download{!isMobile && ` as JSON`}
        </Button>
        <Button
          variant="contained"
          color="primary"
          onClick={handleSaveClick}
        >
          Save{!isMobile && ` Invoice`}
        </Button>
      </Box>
    </Box>
  );
};

Frontend/InvoiceItemsTable.js

import React, { useState } from "react";
import {
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableHead,
  TableRow,
  Paper,
  TextField,
  IconButton,
  Button,
  Box,
  useMediaQuery,
  useTheme,
  Card,
  CardContent,
  Typography,
  Grid,
  Divider,
  Stack
} from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import AddIcon from "@mui/icons-material/Add";

export const InvoiceItemsTable = ({ items = [], onItemsChange }) => {
  const theme = useTheme();
  const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
  
  const [editableItems, setEditableItems] = useState(items);

  const handleItemChange = (index, field, value) => {
    const newItems = [...editableItems];
    newItems[index] = {
      ...newItems[index],
      [field]: value
    };
    
    setEditableItems(newItems);
    onItemsChange(newItems);
  };

  const handleAddItem = () => {
    const newItem = {
      no: editableItems.length + 1,
      description: "",
      unit: "",
      unit_price_without_vat: "",
      unit_price_with_vat: "",
      quantity: "",
      total_without_vat: "",
      total_with_vat: ""
    };
    
    const newItems = [...editableItems, newItem];
    setEditableItems(newItems);
    onItemsChange(newItems);
  };

  const handleDeleteItem = (index) => {
    const newItems = editableItems.filter((_, i) => i !== index);
    
    // Renumber the items
    const renumberedItems = newItems.map((item, i) => ({
      ...item,
      no: i + 1
    }));
    
    setEditableItems(renumberedItems);
    onItemsChange(renumberedItems);
  };

  // Define columns based on screen size
  const getColumns = () => {
    if (isMobile) {
      return [
        { id: "no", label: "#", width: "40px" },
        { id: "description", label: "Description", width: "auto" },
        { id: "quantity", label: "Qty", width: "60px" },
        { id: "total_with_vat", label: "Total", width: "70px" },
        { id: "actions", label: "", width: "40px" }
      ];
    }
    
    return [
      { id: "no", label: "#", width: "30px" },
      { id: "description", label: "Description", width: "30%" },
      { id: "unit", label: "Unit", width: "70px" },
      { id: "quantity", label: "Qty", width: "70px" },
      { id: "unit_price_without_vat", label: "Price (excl)", width: "100px" },
      { id: "unit_price_with_vat", label: "Price (incl)", width: "100px" },
      { id: "total_without_vat", label: "Total (excl)", width: "100px" },
      { id: "total_with_vat", label: "Total (incl)", width: "100px" },
      { id: "actions", label: "", width: "40px" }
    ];
  };

  const columns = getColumns();

  // Mobile Card View
  const renderMobileCards = () => {
    return (
      <Stack spacing={2}>
        {editableItems.map((item, index) => (
          <Card key={index} variant="outlined" sx={{ position: 'relative' }}>
            <CardContent sx={{ pb: 1 }}>
              <Box sx={{ position: 'absolute', top: 8, right: 8 }}>
                <IconButton 
                  size="small" 
                  color="error" 
                  onClick={() => handleDeleteItem(index)}
                >
                  <DeleteIcon fontSize="small" />
                </IconButton>
              </Box>
              
              <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
                Item #{item.no}
              </Typography>
              
              <TextField
                fullWidth
                label="Description"
                variant="outlined"
                size="small"
                value={item.description || ""}
                onChange={(e) => handleItemChange(index, "description", e.target.value)}
                sx={{ mb: 2 }}
              />
              
              <Grid container spacing={2}>
                <Grid item xs={6}>
                  <TextField
                    fullWidth
                    label="Unit"
                    variant="outlined"
                    size="small"
                    value={item.unit || ""}
                    onChange={(e) => handleItemChange(index, "unit", e.target.value)}
                  />
                </Grid>
                <Grid item xs={6}>
                  <TextField
                    fullWidth
                    label="Quantity"
                    variant="outlined"
                    size="small"
                    value={item.quantity || ""}
                    onChange={(e) => handleItemChange(index, "quantity", e.target.value)}
                  />
                </Grid>
              </Grid>
              
              <Divider sx={{ my: 2 }} />
              
              <Grid container spacing={2}>
                <Grid item xs={6}>
                  <TextField
                    fullWidth
                    label="Unit Price (excl VAT)"
                    variant="outlined"
                    size="small"
                    value={item.unit_price_without_vat || ""}
                    onChange={(e) => handleItemChange(index, "unit_price_without_vat", e.target.value)}
                  />
                </Grid>
                <Grid item xs={6}>
                  <TextField
                    fullWidth
                    label="Unit Price (incl VAT)"
                    variant="outlined"
                    size="small"
                    value={item.unit_price_with_vat || ""}
                    onChange={(e) => handleItemChange(index, "unit_price_with_vat", e.target.value)}
                  />
                </Grid>
              </Grid>
              
              <Divider sx={{ my: 2 }} />
              
              <Grid container spacing={2}>
                <Grid item xs={6}>
                  <TextField
                    fullWidth
                    label="Total (excl VAT)"
                    variant="outlined"
                    size="small"
                    value={item.total_without_vat || ""}
                    onChange={(e) => handleItemChange(index, "total_without_vat", e.target.value)}
                  />
                </Grid>
                <Grid item xs={6}>
                  <TextField
                    fullWidth
                    label="Total (incl VAT)"
                    variant="outlined"
                    size="small"
                    value={item.total_with_vat || ""}
                    onChange={(e) => handleItemChange(index, "total_with_vat", e.target.value)}
                  />
                </Grid>
              </Grid>
            </CardContent>
          </Card>
        ))}
      </Stack>
    );
  };

  // Desktop Table View
  const renderDesktopTable = () => {
    return (
      <TableContainer component={Paper}>
        <Table size="small" padding="none" sx={{ '& .MuiTableCell-root': { padding: '4px' } }}>
          <TableHead>
            <TableRow>
              {columns.map((column) => (
                <TableCell 
                  key={column.id} 
                  style={{ width: column.width }}
                  align={column.id === "no" ? "center" : "left"}
                >
                  {column.label}
                </TableCell>
              ))}
            </TableRow>
          </TableHead>
          <TableBody>
            {editableItems.map((item, index) => (
              <TableRow key={index}>
                {columns.map((column) => {
                  if (column.id === "actions") {
                    return (
                      <TableCell key={column.id} sx={{ padding: '0 4px' }}>
                        <IconButton 
                          size="small" 
                          color="error" 
                          onClick={() => handleDeleteItem(index)}
                        >
                          <DeleteIcon fontSize="small" />
                        </IconButton>
                      </TableCell>
                    );
                  }
                  
                  if (column.id === "no") {
                    return (
                      <TableCell key={column.id} align="center">
                        {item.no}
                      </TableCell>
                    );
                  }
                  
                  if (column.id in item) {
                    return (
                      <TableCell key={column.id}>
                        <TextField
                          fullWidth
                          variant="outlined"
                          size="small"
                          value={item[column.id] || ""}
                          onChange={(e) => 
                            handleItemChange(index, column.id, e.target.value)
                          }
                          InputProps={{
                            sx: { 
                              '& .MuiOutlinedInput-input': {
                                padding: '6px 8px',
                                fontSize: '0.9rem'
                              }
                            }
                          }}
                        />
                      </TableCell>
                    );
                  }
                  
                  return <TableCell key={column.id}></TableCell>;
                })}
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </TableContainer>
    );
  };

  return (
    <Box>
      {isMobile ? renderMobileCards() : renderDesktopTable()}
      
      <Box sx={{ mt: 2, display: "flex", justifyContent: "flex-end" }}>
        <Button
          variant="outlined"
          startIcon={<AddIcon />}
          onClick={handleAddItem}
          size={isMobile ? "small" : "medium"}
        >
          Add Item
        </Button>
      </Box>
    </Box>
  );
};

Frontend/ProductsTable.js

import React from "react";
import { Avatar, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, useMediaQuery, useTheme } from "@mui/material";
import PlayArrowIcon from '@mui/icons-material/PlayArrow';

export const ProductsTable = ({ products, handleClick }) => {
    if (!products || products.length === 0) return <div>No products available</div>;

    const theme = useTheme();
    const isMobile = useMediaQuery(theme.breakpoints.down('sm'));

    const columnKeys = Object.keys(products[0]);
    const mobileColumnKeys = ['name', 'price'];
    const capitalizeFirstLetter = (string) => string.charAt(0).toUpperCase() + string.slice(1);

    console.log(products)
    return (
        <TableContainer component={Paper} sx={{ marginTop: 2, maxWidth: '100%', overflowX: 'auto' }}>
            <Table size={isMobile ? "small" : "medium"}>
                <TableHead>
                    <TableRow>
                        <TableCell sx={{ padding: '4px' }}></TableCell>
                        {(isMobile ? mobileColumnKeys : columnKeys).map((key) => (
                            <TableCell key={key}>{capitalizeFirstLetter(key)}</TableCell>
                        ))}
                    </TableRow>
                </TableHead>
                <TableBody>
                    {products.map((product) => (
                        <TableRow key={product.id}>
                            <TableCell sx={{ padding: 2 }}>
                                <IconButton
                                    color="primary"
                                    onClick={() => {
                                        handleClick(product)
                                    }}
                                    size="small"
                                    sx={{
                                        padding: '4px',
                                        backgroundColor: 'rgba(0, 0, 0, 0.1)',
                                        borderRadius: '50%',
                                        '&:hover': {
                                            backgroundColor: 'rgba(0, 0, 0, 0.2)',
                                        },
                                    }}
                                >
                                    <PlayArrowIcon fontSize="small" />
                                </IconButton>
                            </TableCell>
                            {(isMobile ? mobileColumnKeys : columnKeys).map((key) => (
                                <TableCell key={key}>
                                    {key === 'image' ?
                                        <Avatar alt={product.name} src={product[key]} variant="square" /> :
                                        (key === 'name' && isMobile ?
                                                <div>
                                                    {product[key]}
                                                    <br />
                                                    <small style={{color: 'gray'}}>{product.description}</small>
                                                </div> :
                                                product[key]
                                        )
                                    }
                                </TableCell>
                            ))}
                        </TableRow>
                    ))}
                </TableBody>
            </Table>
        </TableContainer>
    );
}

Frontend/contentRender.js

import React, { useEffect } from "react";
import Button from '@mui/material/Button';
import Avatar from "@mui/material/Avatar";
import { InvoiceEditor } from "./InvoiceEditor";

const isMobile = window.openkbs.isMobile;

// Use a regular expression to replace newlines only within string values
const escapeNewlines = (jsonString) => jsonString.replace(/"(?:\\.|[^"\\])*"/g, (match) => match.replace(/\n/g, '\\n'));

const extractJSONFromContent = (content) => {
  try {
    // Check if content is already valid JSON
    JSON.parse(content);
    return { data: JSON.parse(content), prefix: "" };
  } catch (e) {
    // Try to extract JSON from text
    try {
      // Look for JSON pattern in content
      const jsonMatch = content.match(/(\{[\s\S]*\})/);
      if (jsonMatch) {
        const jsonString = escapeNewlines(jsonMatch[0]);
        const data = JSON.parse(jsonString);
        let prefix = content.substring(0, content.indexOf(jsonMatch[0])).trim();
        prefix = prefix.replace(/```json\s*$/i, '').replace(/```\s*$/i, '').trim();
        return { data, prefix };
      }
    } catch (error) {
      console.error("Error extracting JSON:", error);
    }
  }
  return null;
};

const onRenderChatMessage = async (params) => {
  const { APIResponseComponent, theme, setBlockingLoading, setSystemAlert, RequestChatAPI,
    kbUserData, generateMsgId, messages, msgIndex } = params;

  const { content, role } = messages[msgIndex];
  
  if (role === 'user') return; // use default rendering for user messages
  
  // Continue with the original JSON extraction
  const jsonResult = extractJSONFromContent(content);

  if (jsonResult) {
    const { data, prefix } = jsonResult;
    
    // Check if this is an invoice data object
    if (data.invoice) {
      const imageUrl = data.image;
      const avatarSize = isMobile ? '48px' : '64px';

      return [
        imageUrl && (
          <Avatar
            alt="Invoice Image"
            src={imageUrl}
            style={{
              marginRight: '10px',
              cursor: 'pointer',
              position: 'absolute',
              left: isMobile ? -54 : -72,
              bottom: 68,
              borderRadius: '50%',
              height: avatarSize,
              width: avatarSize
            }}
            onClick={() => window.open(imageUrl, '_blank')}
          />
        ),
        prefix && <div style={{ whiteSpace: 'pre-wrap', marginBottom: '10px' }}>{prefix}</div>,
        <InvoiceEditor 
          invoiceData={data} 
          onSave={async (updatedData) => {
            await RequestChatAPI([...messages, {
              role: 'user',
              content: JSON.stringify({
                type: "SAVE_INVOICE_REQUEST",
                ...updatedData
              }),
              userId: kbUserData().chatUsername,
              msgId: generateMsgId()
            }]);
          }}
        />
      ];
    }
    
    // Handle different response types
    if (data.type) {
      switch (data.type) {
        case "SAVE_INVOICE_REQUEST":
          return renderAPIResponse('Save Invoice Request', null, data, prefix);
        case "SAVE_INVOICE_SUCCESS":
          return renderAPIResponse('Invoice Saved', theme?.palette?.success?.main, data, prefix);
        case "SAVE_INVOICE_FAILED":
          return renderAPIResponse('Invoice Save Failed', theme?.palette?.error?.main, data, prefix);
        default:
          // Fallback to generic JSON display
          return renderAPIResponse(data.type || 'API Response', null, data, prefix);
      }
    }
    
    // Default rendering for any JSON
    return renderAPIResponse('API Response', null, data, prefix);
  }
  
  // If no JSON was found, return null to let default rendering handle it
  return null;

  function renderAPIResponse(entityName, color, data, prefix = '') {
    return (
      <>
        {prefix && <div style={{ whiteSpace: 'pre-wrap' }}>{prefix}</div>}
        <APIResponseComponent
          entityName={entityName}
          color={color}
          open={true}
          JSONData={data}
        />
      </>
    );
  }
};

const Header = ({ setRenderSettings }) => {
  useEffect(() => {
    setRenderSettings({
      setMessageWidth: () => isMobile ? '95%' : '85%',
      inputLabelsQuickSend: true,
      disableBalanceView: false,
      disableSentLabel: false,
      disableChatAvatar: isMobile,
      disableChatModelsSelect: false,
      disableContextItems: false,
      disableCopyButton: false,
      disableEmojiButton: false,
      disableTextToSpeechButton: false,
      disableMobileLeftButton: false,
    });
  }, [setRenderSettings]);
};

const exports = { onRenderChatMessage, Header };
window.contentRender = exports;
export default exports;

Frontend/contentRender.json

{
  "dependencies": {
    "react": "^18.2.0 (fixed)",
    "react-dom": "^18.2.0 (fixed)",
    "@mui/material": "^5.16.1 (fixed)",
    "@mui/icons-material": "^5.16.1 (fixed)",
    "@emotion/react": "^11.10.6 (fixed)",
    "@emotion/styled": "^11.10.6 (fixed)"
  }
}