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