Fancy contracts and invoices, display parsed invoices and contracts in UI

This commit is contained in:
Laurie Voss
2025-04-30 10:28:34 -07:00
parent e9ea89ad9b
commit 963b6b7d2e
32 changed files with 125 additions and 15 deletions
BIN
View File
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.
BIN
View File
Binary file not shown.
+56 -3
View File
@@ -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",
+2
View File
@@ -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"
},
+2 -1
View File
@@ -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) {
+25 -5
View File
@@ -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">