mirror of
https://github.com/run-llama/invoice-reconciler.git
synced 2026-07-01 21:14:03 -04:00
Fancy contracts and invoices, display parsed invoices and contracts in UI
This commit is contained in:
Vendored
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
BIN
Binary file not shown.
Generated
+56
-3
@@ -20,6 +20,8 @@
|
||||
"next": "14.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
@@ -1447,14 +1449,12 @@
|
||||
"version": "15.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
||||
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz",
|
||||
"integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@@ -1971,7 +1971,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
@@ -2687,6 +2686,16 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/html-url-attributes": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
||||
"integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/html-void-elements": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
||||
@@ -4431,6 +4440,33 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
||||
"integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"hast-util-to-jsx-runtime": "^2.0.0",
|
||||
"html-url-attributes": "^3.0.0",
|
||||
"mdast-util-to-hast": "^13.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18",
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-medium-image-zoom": {
|
||||
"version": "5.2.14",
|
||||
"resolved": "https://registry.npmjs.org/react-medium-image-zoom/-/react-medium-image-zoom-5.2.14.tgz",
|
||||
@@ -4611,6 +4647,23 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-rehype": {
|
||||
"version": "11.1.2",
|
||||
"resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
|
||||
"integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-to-hast": "^13.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-stringify": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
"next": "14.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
|
||||
@@ -39,7 +39,8 @@ export async function GET(request: NextRequest) {
|
||||
const contracts = data.map((doc: any) => ({
|
||||
id: doc.id,
|
||||
friendlyName: doc.metadata?.friendlyName || null,
|
||||
fileName: doc.metadata?.file_name || 'Unknown file'
|
||||
fileName: doc.metadata?.file_name || 'Unknown file',
|
||||
markdown: doc.text
|
||||
}));
|
||||
|
||||
return NextResponse.json(contracts);
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function GET(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch the parsed document from LlamaIndex API
|
||||
// Fetch the parsed document from LlamaParse API
|
||||
console.log(`Fetching parsed document for job: ${jobId}`);
|
||||
|
||||
const llamaIndexApiUrl = `https://api.cloud.llamaindex.ai/api/v1/parsing/job/${jobId}/result/markdown`;
|
||||
@@ -90,18 +90,28 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
console.log('Successfully retrieved parsed document');
|
||||
|
||||
// extract just the company name from the markdown
|
||||
const nameResponse = await llm.complete({
|
||||
prompt: `Extract the company name from the following markdown: ${markdown}
|
||||
Return ONLY the name of the company as a string, with no other text.
|
||||
Don't use markdown.`
|
||||
})
|
||||
|
||||
const companyName = nameResponse.text.trim();
|
||||
console.log('Successfully extracted company name:', companyName);
|
||||
|
||||
// Call the retriever API to get matching document
|
||||
console.log('Calling retriever API to find matching document');
|
||||
console.log('Calling retriever API to find a contract that matches this company name');
|
||||
|
||||
const retrieverApiUrl = `https://api.cloud.llamaindex.ai/api/v1/retrievers/retrieve?project_id=${projectId}&organization_id=${organizationId}`;
|
||||
|
||||
const retrieverPayload = {
|
||||
mode: "full",
|
||||
query: markdown,
|
||||
query: companyName,
|
||||
pipelines: [
|
||||
{
|
||||
name: "Invoice Matching Pipeline",
|
||||
name: "Contract Matching Pipeline",
|
||||
description: "Find matching contract for invoice reconciliation",
|
||||
pipeline_id: indexId
|
||||
}
|
||||
@@ -136,7 +146,7 @@ export async function GET(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Retriever data:", JSON.stringify(retrieverData, null, 2));
|
||||
//console.log("Retriever data:", JSON.stringify(retrieverData, null, 2));
|
||||
|
||||
// Extract document ID from the first node
|
||||
const firstNode = retrieverData.nodes[0];
|
||||
@@ -218,6 +228,9 @@ export async function GET(request: NextRequest) {
|
||||
// Add documentId to the reconciliation result
|
||||
reconciliationResult.contractId = documentId;
|
||||
|
||||
// add the markdown to the reconciliation result
|
||||
reconciliationResult.markdown = markdown;
|
||||
|
||||
// Come up with a friendly name for the contract
|
||||
const friendlyNameResponse = await llm.complete({
|
||||
prompt: `Come up with a very brief name for this invoice,
|
||||
@@ -233,6 +246,8 @@ export async function GET(request: NextRequest) {
|
||||
const friendlyName = friendlyNameResponse.text.trim();
|
||||
reconciliationResult.friendlyName = friendlyName;
|
||||
|
||||
console.log("Reconciliation result:", JSON.stringify(reconciliationResult, null, 2));
|
||||
|
||||
// Return success response with the enhanced reconciliation result
|
||||
return NextResponse.json(reconciliationResult);
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,18 +4,25 @@ import { useState, useEffect } from 'react';
|
||||
import { useInvoices } from '@/context/InvoicesContext';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import InvoiceUpload from './InvoiceUpload';
|
||||
import ReconciledInvoicesList from './ReconciledInvoicesList';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
interface Contract {
|
||||
id: string;
|
||||
friendlyName: string | null;
|
||||
fileName: string;
|
||||
markdown: string;
|
||||
}
|
||||
|
||||
export default function ContractsList() {
|
||||
const [contracts, setContracts] = useState<Contract[]>([]);
|
||||
const [error, setError] = useState('');
|
||||
const [openContractId, setOpenContractId] = useState<string | null>(null);
|
||||
const { reconciledInvoices } = useInvoices();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -88,14 +95,27 @@ export default function ContractsList() {
|
||||
)}
|
||||
|
||||
{contracts.map(contract => (
|
||||
<Card key={contract.id}>
|
||||
<CardHeader>
|
||||
<Card key={contract.id} className="space-y-0">
|
||||
<CardHeader className="pb-0">
|
||||
<div className="flex justify-between items-center">
|
||||
<CardTitle>{contract.friendlyName || "Unnamed contract"}</CardTitle>
|
||||
<CardDescription className="text-right">{contract.fileName}</CardDescription>
|
||||
<CardTitle className="text-lg">{contract.friendlyName || "Unnamed contract"}</CardTitle>
|
||||
<CardDescription className="text-right text-sm">{contract.fileName}</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="pt-0">
|
||||
<Collapsible open={openContractId === contract.id} onOpenChange={(isOpen) => setOpenContractId(isOpen ? contract.id : null)}>
|
||||
<CollapsibleTrigger asChild className="-mt-8">
|
||||
<Button variant="ghost" className="w-fit flex items-center gap-1 p-0 hover:bg-transparent text-xs text-gray-500 -ml-4">
|
||||
{openContractId === contract.id ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
||||
<span>Show contract</span>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="mt-4 p-4 bg-gray-50 rounded-lg">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{contract.markdown}</ReactMarkdown>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
{reconciledInvoices[contract.id] && (
|
||||
<div className="mt-4">
|
||||
<h3 className="font-medium mb-2">Reconciled Invoices:</h3>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { ChevronDown, ChevronUp, ChevronRight } from 'lucide-react';
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
interface ReconciledInvoice {
|
||||
status: 'success' | 'failure';
|
||||
@@ -10,6 +12,7 @@ interface ReconciledInvoice {
|
||||
due_date: string;
|
||||
total_due?: number;
|
||||
errors?: string[];
|
||||
markdown?: string;
|
||||
}
|
||||
|
||||
interface ReconciledInvoicesListProps {
|
||||
@@ -33,6 +36,7 @@ export default function ReconciledInvoicesList({ invoices }: ReconciledInvoicesL
|
||||
|
||||
function InvoiceItem({ invoice }: { invoice: ReconciledInvoice }) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [showMarkdown, setShowMarkdown] = useState(false);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@@ -43,6 +47,15 @@ function InvoiceItem({ invoice }: { invoice: ReconciledInvoice }) {
|
||||
<div className="text-sm text-gray-500">
|
||||
Due: {new Date(invoice.due_date).toLocaleDateString()}
|
||||
</div>
|
||||
{invoice.markdown && (
|
||||
<button
|
||||
onClick={() => setShowMarkdown(!showMarkdown)}
|
||||
className="text-gray-500 hover:text-gray-700 flex items-center gap-1 text-sm mt-1"
|
||||
>
|
||||
{showMarkdown ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
<span>Show Invoice</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -65,6 +78,12 @@ function InvoiceItem({ invoice }: { invoice: ReconciledInvoice }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showMarkdown && invoice.markdown && (
|
||||
<div className="mt-4 p-4 bg-gray-50 rounded-lg">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{invoice.markdown}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpanded && invoice.errors && invoice.errors.length > 0 && (
|
||||
<div className="mt-2 pl-4 border-l-2 border-gray-200">
|
||||
<ul className="list-disc space-y-1">
|
||||
|
||||
Reference in New Issue
Block a user