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.
Always output all items from the invoice, even if there are hundreds.


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]);
      
      // Return success response
      return {
        type: "SAVE_INVOICE_SUCCESS",
        message: `Invoice with ${requestData?.invoice?.items?.length} items has been successfully saved.`,
        nextStep: `Navigate to the Database section to view your invoices.`,
        ...meta
      };
    } catch (e) {
      console.error("Error saving invoice:", e);
      
      // Return error response
      return {
        type: "SAVE_INVOICE_FAILED",
        error: e.message || "Failed to save invoice",
        ...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, useEffect } 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 { InvoiceItemsTable } from "./InvoiceItemsTable";

const isMobile = window.openkbs.isMobile;

export const InvoiceEditor = ({ invoiceData, onSave }) => {
  // Initialize state with existing invoice data or empty object
  const [invoice, setInvoice] = useState(() => {
    // Keep the itemId if it exists
    const initialInvoice = invoiceData.invoice || {};
    return initialInvoice;
  });

  // Recalculate totals whenever invoice items change
  useEffect(() => {
    if (invoice.items && invoice.items.length > 0) {
      calculateTotals();
    }
  }, [invoice.items]);

  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 calculateTotals = () => {
    const items = invoice.items || [];
    const calculatedSummary = {
      base_total: 0,
      vat_amount: 0,
      total: 0
    };
    
    if (items.length > 0) {
      // Calculate totals based on invoice items
      calculatedSummary.base_total = items.reduce((sum, item) => {
        const itemTotal = parseFloat(item.total_without_vat) || 0;
        return sum + itemTotal;
      }, 0);
      
      calculatedSummary.total = items.reduce((sum, item) => {
        const itemTotal = parseFloat(item.total_with_vat) || 0;
        return sum + itemTotal;
      }, 0);
      
      calculatedSummary.vat_amount = calculatedSummary.total - calculatedSummary.base_total;
      
      // Calculate VAT rate if possible
      if (calculatedSummary.base_total > 0) {
        calculatedSummary.vat_rate = (calculatedSummary.vat_amount / calculatedSummary.base_total * 100).toFixed(2);
      }
    }
    
    const currentSummary = invoice.summary || {};
    
    // Update the summary only if values are not manually set
    setInvoice(prev => ({
      ...prev,
      summary: {
        ...currentSummary,
        base_total: currentSummary.base_total || calculatedSummary.base_total.toFixed(2),
        vat_amount: currentSummary.vat_amount || calculatedSummary.vat_amount.toFixed(2),
        total: currentSummary.total || calculatedSummary.total.toFixed(2),
        vat_rate: currentSummary.vat_rate || calculatedSummary.vat_rate || "",
        amount_due: currentSummary.amount_due || calculatedSummary.total.toFixed(2),
        currency: currentSummary.currency || "USD",
        prepaid_voucher: currentSummary.prepaid_voucher || "0.00"
      }
    }));
  };

  const handleSaveClick = () => {
    // Make sure we have the latest totals calculated
    calculateTotals();
    
    // Need to use the current state reference to ensure we have the latest data
    onSave({
      invoice: invoice
    });
  };

  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 !== undefined ? 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 !== undefined ? 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 !== undefined ? 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 !== undefined ? 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 !== undefined ? 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 !== undefined ? 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="primary"
              onClick={handleSaveClick}
          >
            Save
          </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 !== undefined ? 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 !== undefined ? 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 !== undefined ? 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 !== undefined ? 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 !== undefined ? 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 !== undefined ? 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 !== undefined ? 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] !== undefined ? 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);
    
    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, useState } from "react";
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;
};

// Function to convert nested invoice to flat structure
const flattenInvoice = (invoice) => {
  const flatInvoice = {
    // Basic invoice info
    invoiceNumber: invoice.number || '',
    invoiceDate: invoice.date || '',
    invoicePlace: invoice.place || '',
    
    // Seller info
    sellerName: invoice.seller?.name || '',
    sellerAddress: invoice.seller?.address || '',
    sellerTIN: invoice.seller?.TIN || '',
    sellerVAT: invoice.seller?.VAT || '',
    sellerRepresentative: invoice.seller?.representative || '',
    
    // Buyer info
    buyerName: invoice.buyer?.name || '',
    buyerAddress: invoice.buyer?.address || '',
    buyerTIN: invoice.buyer?.TIN || '',
    buyerVAT: invoice.buyer?.VAT || '',
    buyerContact: invoice.buyer?.contact || '',
    buyerClientNumber: invoice.buyer?.client_number || '',
    
    // Summary info
    baseTotal: invoice.summary?.base_total || '0.00',
    vatRate: invoice.summary?.vat_rate || '0.00',
    vatAmount: invoice.summary?.vat_amount || '0.00',
    totalAmount: invoice.summary?.total || '0.00',
    prepaidVoucher: invoice.summary?.prepaid_voucher || '0.00',
    amountDue: invoice.summary?.amount_due || '0.00',
    currency: invoice.summary?.currency || 'USD',
    
    // Payment info
    paymentType: invoice.payment?.type || '',
    paymentBank: invoice.payment?.bank || '',
    paymentIBAN: invoice.payment?.IBAN || '',
    paymentBIC: invoice.payment?.BIC || '',
    
    // System fields
    itemId: invoice.number,
    items: invoice.items || []
  };
  
  return flatInvoice;
};

// Function to flatten invoice item
const flattenInvoiceItem = (item, invoiceId) => {
  return {
    itemId: invoiceId,
    invoiceId: invoiceId,
    itemNumber: item.no || 0,
    itemDescription: item.description || '',
    itemUnit: item.unit || '',
    itemQuantity: item.quantity || 0,
    itemUnitPriceExVat: item.unit_price_without_vat || 0,
    itemUnitPriceIncVat: item.unit_price_with_vat || 0,
    itemTotalExVat: item.total_without_vat || 0,
    itemTotalIncVat: item.total_with_vat || 0,
    createdAt: item.createdAt,
    updatedAt: item.updatedAt
  };
};

// Function to save an invoice to the database
const saveInvoice = async (invoice, itemsAPI, KB, setSystemAlert) => {
  try {
    // Add timestamp
    const timestamp = new Date().getTime();
    invoice.createdAt = invoice.createdAt || timestamp;
    invoice.updatedAt = timestamp;
    
    // Flatten the invoice for storage
    const flatInvoice = flattenInvoice(invoice);
    flatInvoice.createdAt = invoice.createdAt;
    flatInvoice.updatedAt = invoice.updatedAt;

    // Save the main invoice record
    await itemsAPI.createItem({
      itemId: invoice.number,
      itemType: 'invoice',
      KBData: KB,
      attributes: [
        { attrName: 'invoiceNumber', attrType: 'text1' },
        { attrName: 'invoiceDate', attrType: 'text2' },
        { attrName: 'invoicePlace', attrType: 'text3' },
        { attrName: 'sellerName', attrType: 'text4' },
        { attrName: 'sellerAddress', attrType: 'text5' },
        { attrName: 'sellerTIN', attrType: 'text6' },
        { attrName: 'sellerVAT', attrType: 'text7' },
        { attrName: 'sellerRepresentative', attrType: 'text8' },
        { attrName: 'buyerName', attrType: 'text9' },
        { attrName: 'buyerAddress', attrType: 'keyword8' },
        { attrName: 'buyerTIN', attrType: 'keyword7' },
        { attrName: 'buyerVAT', attrType: 'keyword6' },
        { attrName: 'buyerContact', attrType: 'keyword5' },
        { attrName: 'buyerClientNumber', attrType: 'keyword4' },
        { attrName: 'baseTotal', attrType: 'float1' },
        { attrName: 'vatRate', attrType: 'float2' },
        { attrName: 'vatAmount', attrType: 'float3' },
        { attrName: 'totalAmount', attrType: 'float4' },
        { attrName: 'prepaidVoucher', attrType: 'float5' },
        { attrName: 'amountDue', attrType: 'float6' },
        { attrName: 'currency', attrType: 'keyword1' },
        { attrName: 'paymentType', attrType: 'keyword2' },
        { attrName: 'paymentBank', attrType: 'keyword3' },
        { attrName: 'paymentIBAN', attrType: 'keyword9' },
        { attrName: 'createdAt', attrType: 'integer1' },
        { attrName: 'updatedAt', attrType: 'integer2' }
      ],
      item: flatInvoice
    });

    // Save each invoice item
    if (invoice.items && invoice.items.length > 0) {
      for (const item of invoice.items) {

        // Add relation to parent invoice and timestamps
        item.invoiceId = invoice.number;
        item.createdAt = item.createdAt || timestamp;
        item.updatedAt = timestamp;
        
        // Flatten the item for storage
        const flatItem = flattenInvoiceItem(item, invoice.number);
        flatItem.createdAt = item.createdAt;
        flatItem.updatedAt = item.updatedAt;

        try {
          // Save the invoice item
          itemsAPI.createItem({
            itemId: `${invoice.number}_${item.no}`,
            itemType: 'item',
            KBData: KB,
            attributes: [
              { attrName: 'itemNumber', attrType: 'integer5' },
              { attrName: 'itemDescription', attrType: 'text1' },
              { attrName: 'itemUnit', attrType: 'text2' },
              { attrName: 'itemQuantity', attrType: 'float1' },
              { attrName: 'itemUnitPriceExVat', attrType: 'float2' },
              { attrName: 'itemUnitPriceIncVat', attrType: 'float3' },
              { attrName: 'itemTotalExVat', attrType: 'float4' },
              { attrName: 'itemTotalIncVat', attrType: 'float5' },
              { attrName: 'invoiceId', attrType: 'text3' },
              { attrName: 'createdAt', attrType: 'integer3' },
              { attrName: 'updatedAt', attrType: 'integer4' }
            ],
            item: flatItem
          });
        } catch (err) {}

      }
    }

    setSystemAlert({
      severity: 'success',
      message: 'Invoice saved successfully!'
    });
    
    return invoice.itemId;
  } catch (error) {
    console.error("Error saving invoice:", error);
    setSystemAlert({
      severity: 'error',
      message: 'Failed to save invoice. Please try again.'
    });
    throw error;
  }
};


const onRenderChatMessage = async (params) => {
  const { APIResponseComponent, theme, setBlockingLoading, setSystemAlert, RequestChatAPI,
    kbUserData, generateMsgId, messages, msgIndex, itemsAPI, KB } = 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) => {
            setBlockingLoading(true);
            try {
              // Save invoice directly to the database
              const invoiceId = await saveInvoice(updatedData.invoice, itemsAPI, KB, setSystemAlert);
              
              // Send a message to the chat about the successful save
              await RequestChatAPI([...messages, {
                role: 'user',
                content: JSON.stringify({
                  type: "SAVE_INVOICE_REQUEST",
                  invoiceId: invoiceId,
                  invoice: updatedData.invoice
                }),
                userId: kbUserData().chatUsername,
                msgId: generateMsgId()
              }]);
            } catch (e) {
              console.error("Error in save process:", e);
            } finally {
              setBlockingLoading(false);
            }
          }}
        />
      ];
    }
    
    // Handle different response types
    if (data.type) {
      switch (data.type) {
        case "SAVE_INVOICE_REQUEST":
          // Just acknowledge the save request
          return renderAPIResponse('Invoice Saved', theme?.palette?.success?.main, data, prefix);
          
        case "SAVE_INVOICE_SUCCESS":
        case "SAVE_INVOICE_FAILED":
        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)"
  }
}