
Invoice Reader
AI Invoice Extraction Tool


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)"
}
}