From fb0d93dff13875e16bfeca064357bb9f5891f332 Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Mon, 18 Mar 2024 11:34:32 -0400 Subject: [PATCH] x --- frontend/index.html | 16 + frontend/package.json | 61 + frontend/postcss.config.js | 6 + frontend/src/App.tsx | 66 + frontend/src/api.tsx | 72 + frontend/src/components/CreateExtractor.tsx | 179 + frontend/src/components/Extractor.tsx | 59 + frontend/src/components/Playground.tsx | 73 + frontend/src/components/ResultsTable.tsx | 56 + frontend/src/components/Sidebar.tsx | 81 + frontend/src/index.css | 3 + frontend/src/main.tsx | 7 + frontend/src/vite-env.d.ts | 1 + frontend/tailwind.config.js | 19 + frontend/tsconfig.json | 25 + frontend/tsconfig.node.json | 11 + frontend/vite.config.ts | 18 + frontend/yarn.lock | 6314 +++++++++++++++++++ 18 files changed, 7067 insertions(+) create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api.tsx create mode 100644 frontend/src/components/CreateExtractor.tsx create mode 100644 frontend/src/components/Extractor.tsx create mode 100644 frontend/src/components/Playground.tsx create mode 100644 frontend/src/components/ResultsTable.tsx create mode 100644 frontend/src/components/Sidebar.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 frontend/yarn.lock diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..6f5861d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + LangChain Extract + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..db391c2 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,61 @@ +{ + "name": "langchain-extract", + "private": true, + "version": "0.0.0", + "type": "module", + "license": "MIT", + "scripts": { + "fix": "eslint --fix --ext=js,jsx,ts,tsx . && prettier --write .", + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 && prettier --check" + }, + "dependencies": { + "@chakra-ui/card": "^2.2.0", + "@chakra-ui/icons": "^2.1.1", + "@chakra-ui/react": "^2.8", + "@codemirror/lang-json": "^6.0.1", + "@emotion/react": "^11", + "@emotion/styled": "^11", + "@heroicons/react": "^2.1.1", + "@mui/material": "^5.15.10", + "@rjsf/chakra-ui": "^5.17.1", + "@rjsf/core": "^5.17.1", + "@rjsf/utils": "^5.17.1", + "@rjsf/validator-ajv8": "^5.17.1", + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/typography": "^0.5.10", + "@tanstack/react-query": "^5.22.2", + "@uiw/react-codemirror": "^4.21.24", + "axios": "^1.6.7", + "chakra-react-select": "^4.7.6", + "eslint-plugin-react": "^7.34.1", + "framer-motion": "^5", + "orval": "^6.25.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.3", + "react-syntax-highlighter": "^15.5.0", + "swr": "^2.2.5", + "vite-plugin-eslint": "^1.8.1", + "yarn": "^1.22.21" + }, + "devDependencies": { + "@types/react": "^18.2.56", + "@types/react-dom": "^18.2.19", + "@typescript-eslint/eslint-plugin": "^7.0.2", + "@typescript-eslint/parser": "^7.0.2", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.17", + "daisyui": "^4.7.2", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.35", + "tailwindcss": "^3.4.1", + "typescript": "^5.4.2", + "typescript-eslint": "^7.2.0", + "vite": "^5.1.4" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..6d5f978 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,66 @@ +import { ChakraProvider, useDisclosure } from "@chakra-ui/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React from "react"; +import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom"; +import CreateExtractor from "./components/CreateExtractor"; +import { Playground } from "./components/Playground"; +import { Sidebar } from "./components/Sidebar"; +import "./index.css"; + +const queryClient = new QueryClient(); + +const Root = () => { + const { isOpen, onOpen, onClose } = useDisclosure(); + + return ( + <> +
+
+
🦜⛏️ LangChain Extract
+
+ Research Preview: this app is unauthenticated and all data can be found. Do not use with + sensitive data. +
+
+
+
+ +
+
+ +
+
+
+ + ); +}; + +const Main = () => { + return ( + <> + + + }> + } /> + } /> + } /> + + + + + ); +}; + +const App = () => { + return ( + + + +
+ + + + ); +}; + +export default App; diff --git a/frontend/src/api.tsx b/frontend/src/api.tsx new file mode 100644 index 0000000..0060225 --- /dev/null +++ b/frontend/src/api.tsx @@ -0,0 +1,72 @@ +/* Expose API hooks for use in components */ +import axios from "axios"; +import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query"; + +export type ExtractorData = { + uuid: string; + name: string; + description: string; + schema: any; +}; + +const getExtractor = async ({ queryKey }): ExtractorData => { + const [_, uuid] = queryKey; + const response = await axios.get(`/extractors/${uuid}`); + return response.data; +}; + +const listExtractors = async () => { + const response = await axios.get("/extractors"); + return response.data; +}; + +const createExtractor = async (extractor) => { + const response = await axios.post("/extractors", extractor); + return response.data; +}; + +export const suggestExtractor = async ({ description, jsonSchema }) => { + if (description === "") { + return {}; + } + const response = await axios.post("/suggest", { description, jsonSchema }); + return response.data; +}; + +export const runExtraction = async (extractionRequest) => { + const response = await axios.postForm("/extract", extractionRequest); + return response.data; +}; + +export const useRunExtraction = () => { + return useMutation({ mutationFn: runExtraction }); +}; + +export const useGetExtractor = (uuid: string) => { + return useQuery({ queryKey: ["getExtractor", uuid], queryFn: getExtractor }); +}; + +export const useGetExtractors = () => { + return useQuery({ queryKey: ["getExtractors"], queryFn: listExtractors }); +}; + +export const useDeleteExtractor = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (uuid: string) => axios.delete(`/extractors/${uuid}`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["getExtractors"] }); + }, + }); +}; + +export const useCreateExtractor = ({ onSuccess }) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createExtractor, + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["getExtractors"] }); + onSuccess(data); + }, + }); +}; diff --git a/frontend/src/components/CreateExtractor.tsx b/frontend/src/components/CreateExtractor.tsx new file mode 100644 index 0000000..c505b82 --- /dev/null +++ b/frontend/src/components/CreateExtractor.tsx @@ -0,0 +1,179 @@ +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Badge, + Button, + Card, + CardBody, + CircularProgress, + FormControl, + FormLabel, + Heading, + Icon, + IconButton, + Input, + Text, +} from "@chakra-ui/react"; +import { json } from "@codemirror/lang-json"; +import Form from "@rjsf/chakra-ui"; +import validator from "@rjsf/validator-ajv8"; +import CodeMirror from "@uiw/react-codemirror"; +import Ajv from "ajv"; +import React from "react"; +import { useNavigate } from "react-router-dom"; +import { suggestExtractor, useCreateExtractor } from "../api"; + +import { ChatBubbleBottomCenterTextIcon } from "@heroicons/react/24/outline"; +import { useMutation } from "@tanstack/react-query"; + +const ArrowUpIconImported = (props) => { + return ; +}; + +const ajc = new Ajv(); + +/** + * Component to create a new extractor with fields for name, description, schema, and examples + */ +const CreateExtractor = ({}) => { + const startSchema = "{}"; + // You might use a mutation hook here if you're using something like React Query for state management + const [schema, setSchema] = React.useState(startSchema); + const [lastValidSchema, setLastValidSchema] = React.useState(JSON.parse(startSchema)); + const [currentSchemaValid, setCurrentSchemaValid] = React.useState(true); + const [userInput, setUserInput] = React.useState(""); + + const suggestMutation = useMutation({ + mutationFn: suggestExtractor, + onSuccess: (data) => { + let prettySchema = data.json_schema; + + try { + prettySchema = JSON.stringify(JSON.parse(data.json_schema), null, 2); + } catch (e) {} + + setSchema(prettySchema); + }, + }); + + const navigate = useNavigate(); + const { mutate, isLoading } = useCreateExtractor({ + onSuccess: (data) => { + navigate(`/e/${data.uuid}`); + }, + }); + + React.useMemo(() => { + try { + const parsedSchema = JSON.parse(schema); + ajc.compile(parsedSchema); + setCurrentSchemaValid(true); + setLastValidSchema(parsedSchema); + } catch (e) { + setCurrentSchemaValid(false); + } + }, [schema]); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const instruction = ""; + const objectSchema = JSON.parse(schema); + // Extract information from schema like name, and description + const name = objectSchema.title || "Unnamed"; + const description = objectSchema.description || ""; + const shortDescription = description.length > 100 ? description.substring(0, 100) + "..." : description; + + mutate({ name, description: shortDescription, schema: objectSchema, instruction }); + }; + + const handleSuggest = (event: React.FormEvent) => { + event.preventDefault(); + const description = event.currentTarget.userInput.value; + if (description === "") { + return; + } + console.log(`Making request with description: ${description} and schema: ${schema}`); + suggestMutation.mutate({ description, jsonSchema: schema }); + setUserInput(""); + }; + + return ( +
+ + What would you like to extract today? + +
+ + setUserInput(event.target.value)} + > + + {suggestMutation.isPending ? ( + + ) : ( + } aria-label="OK" colorScheme="blue" /> + )} + +
+
OR
+ + + + Edit JSON Schema +
+ {currentSchemaValid ? OK : Errors!} + +
+
+ + + setSchema(value)} + basicSetup={{ autocompletion: true }} + extensions={[json()]} + minHeight="300px" + className="border-4 border-slate-300 border-double" + /> + + +
+
+ {Object.keys(lastValidSchema).length !== 0 && ( + <> + Preview + {!currentSchemaValid && ( + JSON Schema has errors. Showing previous valid JSON Schema. + )} + + + + + + + )} + +
+
+ ); +}; + +export default CreateExtractor; diff --git a/frontend/src/components/Extractor.tsx b/frontend/src/components/Extractor.tsx new file mode 100644 index 0000000..05212c6 --- /dev/null +++ b/frontend/src/components/Extractor.tsx @@ -0,0 +1,59 @@ +import { Tab, TabList, TabPanel, TabPanels, Tabs, Text } from "@chakra-ui/react"; +import Form from "@rjsf/chakra-ui"; +import validator from "@rjsf/validator-ajv8"; +import { useGetExtractor } from "../api"; + +import { VStack } from "@chakra-ui/react"; +import SyntaxHighlighter from "react-syntax-highlighter"; +import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs"; + +export const Extractor = ({ extractorId }: { extractorId: string }) => { + const { data, isLoading, isError } = useGetExtractor(extractorId); + if (isLoading) { + return
Loading...
; + } + if (isError) { + return
Error
; + } + + if (data === undefined) { + throw new Error("Data is undefined"); + } + console.log(data.schema); + + return ( +
+ + + Form + Code + + + +
+ + + + This shows the raw JSON Schema that describes what information the extractor will be extracting from the + content. + + + {JSON.stringify(data.schema, null, 2)} + + + + + + {/* TO DO ADD SYSTEM MESSAGE */} + {/* + System Message: + {data.instruction} + */} + +
+ ); +}; diff --git a/frontend/src/components/Playground.tsx b/frontend/src/components/Playground.tsx new file mode 100644 index 0000000..087a441 --- /dev/null +++ b/frontend/src/components/Playground.tsx @@ -0,0 +1,73 @@ +import { Button, Heading } from "@chakra-ui/react"; +import { useMutation } from "@tanstack/react-query"; +import { runExtraction, useRunExtraction } from "../api"; +import { Extractor } from "./Extractor"; +import { ResultsTable } from "./ResultsTable"; +import { useParams } from "react-router"; +import { Textarea } from '@chakra-ui/react' + +import React from "react"; + +/** + * Playground to work with an existing extractor. + */ +export const Playground = () => { + const { extractorId } = useParams(); + const { data, isPending, mutate } = useMutation({ mutationFn: runExtraction }); + const [isDisabled, setIsDisabled] = React.useState(false); + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + const request = { + extractor_id: extractorId, + }; + + if (event.currentTarget.text.value) { + Object.assign(request, { text: event.currentTarget.text.value }); + } else { + Object.assign(request, { file: event.currentTarget.file.files[0] }); + } + + mutate(request); + }; + + const handleChange = (event: React.FormEvent) => { + if (event.currentTarget.text.value === "" && event.currentTarget.file.files.length === 0) { + setIsDisabled(true); + return; + } else { + // Also disable if both are present + if (event.currentTarget.text.value !== "" && event.currentTarget.file.files.length !== 0) { + setIsDisabled(true); + return; + } + } + + setIsDisabled(false); + }; + + return ( +
+
+
+ +
+ + +
OR
+