From bd052f7ec1c22ad036380b25b61d2a8de166e26b Mon Sep 17 00:00:00 2001 From: Remi Guan Date: Fri, 20 Dec 2024 08:32:04 +0800 Subject: [PATCH 1/8] feat: Key-value extraction --- .vscode/settings.json | 3 + app/actions/runSyncExtractKeyValue.ts | 57 ++++ app/components/playground/ActionContainer.tsx | 8 +- .../ExtractKeyValuePairContainer.tsx | 109 +++++++ app/components/playground/KeyValueInputs.tsx | 273 ++++++++++++++++++ app/types/PlaygroundTypes.ts | 1 + package-lock.json | 24 ++ package.json | 3 + test-results/.last-run.json | 13 +- tests/coreTests.ts | 56 +++- 10 files changed, 541 insertions(+), 6 deletions(-) create mode 100644 app/actions/runSyncExtractKeyValue.ts create mode 100644 app/components/playground/ExtractKeyValuePairContainer.tsx create mode 100644 app/components/playground/KeyValueInputs.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c266075..a4341069 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,4 +8,7 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, + "cSpell.words": [ + "cambio" + ], } \ No newline at end of file diff --git a/app/actions/runSyncExtractKeyValue.ts b/app/actions/runSyncExtractKeyValue.ts new file mode 100644 index 00000000..aa61cbcf --- /dev/null +++ b/app/actions/runSyncExtractKeyValue.ts @@ -0,0 +1,57 @@ +import axios from 'axios'; +import getApiKeysForUser from './account/getApiKeysForUser'; +import { ApiKey } from '../hooks/useAccountStore'; + +interface IParams { + userId: string; + token: string; + apiUrl: string; + base64String: string; + extractInstruction?: Record; + fileType?: string; +} + +export const runSyncTableExtract = async ({ + userId, + token, + apiUrl, + base64String, + extractInstruction, +}: IParams): Promise => { + // Mocking the response for testing + return JSON.stringify({ + personal_info: { + name: 'Mark Henry Nimo', + email: 'marknimo@gmail.com', + address: '3803 Marquette Place 4H, San Diego, CA', + phone: '(310) 621-8089', + }, + account_info: { + account_number: '1234567890', + account_type: 'Checking', + balance: '$10,000.00', + }, + }); + const extractAPI = `${apiUrl}/extract_key_value`; + const params = { + file_content: base64String, + file_type: 'pdf', + extract_instruction: extractInstruction || {}, + }; + + const config = { + headers: { + 'x-api-key': '-', + Authorization: token, + }, + }; + + const keyValueExtractResponse = await axios.post(extractAPI, params, config); + + if (keyValueExtractResponse.status !== 200) { + throw new Error('Failed to extract key value pairs'); + } + + const json = keyValueExtractResponse.data.json[0]; + return json; +}; diff --git a/app/components/playground/ActionContainer.tsx b/app/components/playground/ActionContainer.tsx index 809c3d94..00086255 100644 --- a/app/components/playground/ActionContainer.tsx +++ b/app/components/playground/ActionContainer.tsx @@ -1,11 +1,12 @@ import usePlaygroundStore from '@/app/hooks/usePlaygroundStore'; import LoginComponent from '../auth/Login'; import PlaygroundTab from './PlaygroundTab'; -import ExtractContainer from './ExtractContainer'; import { useEffect, useState } from 'react'; import { PlaygroundFile, PlaygroundTabs } from '@/app/types/PlaygroundTypes'; import UploadButton from './UploadButton'; import MapContainer from './table/MapContainer'; +import ExtractContainer from './ExtractContainer'; +import ExtractKeyValuePairContainer from './ExtractKeyValuePairContainer'; const ActionContainer = () => { const { loggedIn, selectedFileIndex, files } = usePlaygroundStore(); @@ -19,7 +20,7 @@ const ActionContainer = () => { return (
-
+
{Object.values(PlaygroundTabs).map((tab) => ( ))} @@ -33,11 +34,12 @@ const ActionContainer = () => {
) : ( -
+
{(selectedFile?.activeTab === PlaygroundTabs.PLAIN_TEXT || selectedFileIndex === null) && ( )} {selectedFile?.activeTab === PlaygroundTabs.TABLE && } + {selectedFile?.activeTab === PlaygroundTabs.KEY_VALUE_PAIR && }
) ) : ( diff --git a/app/components/playground/ExtractKeyValuePairContainer.tsx b/app/components/playground/ExtractKeyValuePairContainer.tsx new file mode 100644 index 00000000..32ce3b68 --- /dev/null +++ b/app/components/playground/ExtractKeyValuePairContainer.tsx @@ -0,0 +1,109 @@ +import { toast } from 'react-hot-toast'; +import { useEffect, useState } from 'react'; +import { ArrowLeft } from '@phosphor-icons/react'; +import { PlaygroundFile } from '@/app/types/PlaygroundTypes'; +import { useProductionContext } from './ProductionContext'; +import { runSyncTableExtract } from '@/app/actions/runSyncExtractKeyValue'; +import Button from '../Button'; +import CodeBlock from '../CodeBlock'; +import DocumentViewer from '../DocumentViewer'; +import KeyValueInputs from './KeyValueInputs'; +import usePlaygroundStore from '@/app/hooks/usePlaygroundStore'; + +const ExtractKeyValuePairContainer = () => { + const { apiURL } = useProductionContext(); + const { selectedFileIndex, files, updateFileAtIndex, token, userId } = usePlaygroundStore(); + const [selectedFile, setSelectedFile] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [extractedResult, setExtractedResult] = useState(null); + + useEffect(() => { + if (selectedFileIndex !== null && files.length > 0) { + const thisFile = files[selectedFileIndex]; + setSelectedFile(thisFile); + setExtractedResult(null); + } + }, [selectedFileIndex, files, updateFileAtIndex]); + + const getFileUrl = (file: PlaygroundFile) => { + if (!file.file) return ''; + if (typeof file.file === 'string') return file.file; + return URL.createObjectURL(file.file); + }; + + const onSubmit = async (extractInstruction: Record) => { + if (!selectedFile?.file) { + toast.error('Please select a file first'); + return; + } + + try { + setIsLoading(true); + const file = selectedFile.file; + let base64String = ''; + + if (file instanceof File) { + const buffer = await file.arrayBuffer(); + base64String = Buffer.from(buffer).toString('base64'); + } else { + base64String = file; + } + + const result = await runSyncTableExtract({ + userId, + token, + apiUrl: apiURL, + base64String, + fileType: file instanceof File ? file.type : 'pdf', + extractInstruction, + }); + + setExtractedResult(result); + toast.success('Extraction complete!'); + } catch (error) { + toast.error('Extraction failed. Please try again.'); + console.error(error); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ {!extractedResult && selectedFile && ( + + )} + {extractedResult && ( +
+ +
+
+
+ )} +
+
+
+
+ +
+
+
+
+ ); +}; + +export default ExtractKeyValuePairContainer; diff --git a/app/components/playground/KeyValueInputs.tsx b/app/components/playground/KeyValueInputs.tsx new file mode 100644 index 00000000..4e6dcf10 --- /dev/null +++ b/app/components/playground/KeyValueInputs.tsx @@ -0,0 +1,273 @@ +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { FieldErrors, FieldValues, UseFormRegister } from 'react-hook-form'; +import { BiPlus, BiMinus, BiCaretRight } from 'react-icons/bi'; +import toast from 'react-hot-toast'; +import { v4 as uuidv4 } from 'uuid'; +import Button from '../Button'; + +interface InputProps { + id: string; + errors: FieldErrors; + register: UseFormRegister; + onAdd?: () => void; + onRemove?: () => void; + canRemove?: boolean; +} + +interface KeyValueInputsProps { + onSubmit: (extractInstructions: Record) => void; + isLoading?: boolean; +} + +const AddButton = ({ onClick }: { onClick?: () => void }) => { + return ( + + ); +}; + +const RemoveButton = ({ onClick }: { onClick?: () => void }) => { + return ( + + ); +}; + +const ExpandButton = ({ active, onClick }: { active: boolean, onClick: () => void }) => { + return ( + + ); +}; + +const Input = ({ + id, + errors, + register, + onAdd, + onRemove, + canRemove = true, +}: InputProps) => { + const [descriptionExpanded, setDescriptionExpanded] = useState(false); + return ( +
+
+ + {canRemove && } +
+
+ {setDescriptionExpanded(!descriptionExpanded)}} active={descriptionExpanded} /> +
+
+
+ + {errors[`${id}-key`] && ( + + {errors[`${id}-key`]?.message?.toString() || 'Key name is required'} + + )} +
+