diff --git a/package.json b/package.json index 01759d1..6365e7b 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "plot-ai", "version": "0.1.0", "private": true, + "proxy": "http://localhost:3000", "type": "module", "scripts": { "build": "next build", @@ -40,6 +41,7 @@ "next": "^14.2.2", "next-auth": "^4.24.7", "next-themes": "^0.3.0", + "nextjs-cors": "^2.2.0", "radix-ui": "^1.0.1", "react": "18.2.0", "react-dom": "18.2.0", @@ -48,6 +50,7 @@ "server-only": "^0.0.1", "sonner": "^1.4.41", "superjson": "^2.2.1", + "swr": "^2.2.5", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.0" diff --git a/src/app/api/trpc/[trpc]/route.ts b/src/app/api/trpc/[trpc]/route.ts index 7213e81..e7bc5a8 100644 --- a/src/app/api/trpc/[trpc]/route.ts +++ b/src/app/api/trpc/[trpc]/route.ts @@ -1,11 +1,13 @@ import { httpBatchLink } from "@trpc/client"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; -import { type NextRequest } from "next/server"; +import { NextResponse, type NextRequest } from "next/server"; import { env } from "~/env"; import { appRouter } from "~/server/api/root"; import { createTRPCContext } from "~/server/api/trpc"; +import { NextApiResponse } from "next"; + /** * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when * handling a HTTP request (e.g. when you make requests from Client Components). @@ -16,8 +18,8 @@ const createContext = async (req: NextRequest) => { }); }; -const handler = (req: NextRequest) => - fetchRequestHandler({ +const handler = async (req: NextRequest, res: NextApiResponse) => { + return fetchRequestHandler({ endpoint: "/api/trpc", req, router: appRouter, @@ -26,10 +28,11 @@ const handler = (req: NextRequest) => env.NODE_ENV === "development" ? ({ path, error }) => { console.error( - `❌ tRPC failed on ${path ?? ""}: ${error.message}` + `❌ tRPC failed on ${path ?? ""}: ${error.message}`, ); } : undefined, }); +}; export { handler as GET, handler as POST }; diff --git a/src/app/models/_components/gpt-search.tsx b/src/app/models/_components/gpt-search.tsx new file mode 100644 index 0000000..8bccda7 --- /dev/null +++ b/src/app/models/_components/gpt-search.tsx @@ -0,0 +1,174 @@ +// @ts-nocheck +import React, { useState } from "react"; +import { useModelNodesContext } from "~/app/_context/model-context"; +import { Button } from "~/components/ui/button"; +import { api } from "~/trpc/react"; + +interface GptSearchPageProps { + onResults: (results: JSON) => JSON; +} + +const GptSearchPage: React.FC = ({ onResults }) => { + const [query, setQuery] = useState(""); + const [searchResult, setSearchResult] = useState(null); + const fetchProperties = api.propertySearch.search.useMutation(); + const nodes = { + nodes: [ + { + id: "0", + type: "blockComp", + data: [ + { + id: "vy4wuo965iq", + section: "Property Details", + value: "deckArea", + label: "Deck Area", + format: "size", + input: 0, + operator: "", + visible: true, + }, + { + id: "ej2pk6l6lsj", + section: "Property Details", + value: "deckArea", + label: "Deck Area", + format: "size", + input: 0, + operator: "", + visible: true, + }, + { + id: "1yx5rx7c9wfh", + section: "Property Details", + value: "squareFeet", + label: "Square Feet", + format: "size", + input: 0, + operator: "", + visible: true, + }, + { + id: "09ushjuob9px", + section: "Property Details", + value: "unitsCount", + label: "Unit Count", + format: "number", + input: 0, + operator: "", + visible: true, + }, + ], + dragHandle: ".custom-drag-handle", + style: { + border: "2px solid #ddd", + background: "white", + borderRadius: "8px", + }, + position: { x: 0, y: 0 }, + width: 276, + height: 196, + }, + { + id: "1", + type: "blockComp", + dragHandle: ".custom-drag-handle", + position: { x: 311.5153248642067, y: 108.71122718380283 }, + style: { + border: "2px solid #ddd", + background: "white", + borderRadius: "8px", + }, + data: [ + { + id: "3n36fktrp4y", + section: "Financial Valuation", + value: "assessedLandValue", + label: "Assessed Land Value", + format: "USD", + input: 0, + operator: "", + visible: true, + }, + { + id: "raeiz6vfwu", + section: "Financial Valuation", + value: "priorSaleAmount", + label: "Prior Sale Amount", + format: "USD", + input: 0, + operator: "", + visible: true, + }, + { + id: "qoa6conq1o", + section: "Financial Valuation", + value: "assessedValue", + label: "Assessed Value", + format: "USD", + input: 0, + operator: "", + visible: true, + }, + ], + width: 276, + height: 196, + }, + ], + edges: [{ id: "1", source: "0", target: "1" }], + viewport: { + x: -251.76669469951742, + y: -24.53523350178338, + zoom: 1.3571296849803665, + }, + }; + + const handleSubmit: React.FormEventHandler = (e) => { + const result = fetchProperties.mutate({ + nodes: JSON.stringify(nodes), + userInput: query, + }); + console.log(result); + onResults(result); + }; + + return ( +
+

+ Use AI to find properties: +

+
+
+ + setQuery(e.target.value)} + required + /> +
+ +
+ {searchResult && ( +
+

Search Result:

+

{searchResult.result}

+
+ )} +
+ ); +}; + +export default GptSearchPage; + diff --git a/src/app/models/_components/property-card.tsx b/src/app/models/_components/property-card.tsx new file mode 100644 index 0000000..bbc6185 --- /dev/null +++ b/src/app/models/_components/property-card.tsx @@ -0,0 +1,30 @@ +import React from "react"; + +const PropertyCard = ({ imageUrl, price, address, propDetails }) => { + return ( +
+
+
+
+ {address} +
+
+
+

${price}

+

{address}

+
+ {propDetails?.map((param) => ( +

Param

+ ))} +
+
+
+
+ ); +}; + +export default PropertyCard; diff --git a/src/app/models/editor/[modelId]/search/page.tsx b/src/app/models/editor/[modelId]/search/page.tsx new file mode 100644 index 0000000..95beddb --- /dev/null +++ b/src/app/models/editor/[modelId]/search/page.tsx @@ -0,0 +1,42 @@ +// @ts-nocheck +"use client" +import React, { useState } from "react"; +import GptSearchPage from "~/app/models/_components/gpt-search"; +import PropertyCard from "~/app/models/_components/property-card"; + + + +function Page() { + const [propertyData, setPropertyData] = useState([]); + + function onResultUpdate(data) { + // The data returned will be an array of key value pairs ex: ["assessedValue": "10000"] + /* We could possibly format the returned data to be: + + ["address": "example address", + "price": "10000000", + "propDetails": {[]}] + + Then use a map to unravel the data to the PropertyCard component and populate it with whatever params we need + + */ + setPropertyData(data); + }; + + return ( +
+ {propertyData.map((property, index) => ( + + ))} + +
+ ); +} + +export default Page; diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 3e13bfb..23d16f3 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -1,6 +1,7 @@ import { postRouter } from "~/server/api/routers/post"; import { modelsRouter } from "~/server/api/routers/models"; import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; +import { propertySearchRouter } from "./routers/propertySearch"; /** * This is the primary router for your server. @@ -9,7 +10,8 @@ import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; */ export const appRouter = createTRPCRouter({ post: postRouter, - models: modelsRouter + models: modelsRouter, + propertySearch: propertySearchRouter, }); // export type definition of API @@ -23,4 +25,3 @@ export type AppRouter = typeof appRouter; * ^? Post[] */ export const createCaller = createCallerFactory(appRouter); - diff --git a/src/server/api/routers/propertySearch.ts b/src/server/api/routers/propertySearch.ts new file mode 100644 index 0000000..8ff443a --- /dev/null +++ b/src/server/api/routers/propertySearch.ts @@ -0,0 +1,77 @@ +import { z } from "zod"; +import { env } from "~/env"; + +const keys: string[] = [process.env.TEAM_API_KEY!, process.env.OPENAI_API_KEY!]; + +import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; + +export const propertySearchRouter = createTRPCRouter({ + search: protectedProcedure + .input(z.object({ nodes: z.string(), userInput: z.string() })) + .mutation(async ({ input }) => { + const nodeData = JSON.parse(input.nodes); + console.log("DATA==========", input.userInput); + const options: RequestInit = { + method: "POST", + headers: { + accept: "text/plain", + "x-api-key": keys[0], + "x-openai-key": keys[1], + "x-user-id": "UniqueUserIdentifier", + "content-type": "application/json", + }, + body: JSON.stringify({ + size: 25, + query: input.userInput, + }), + }; + + const requestHeaders: HeadersInit = new Headers(); + requestHeaders.set("accept", "text/plain"); + requestHeaders.set("x-api-key", keys[0]); + requestHeaders.set("x-openai-key", keys[1]); + requestHeaders.set("content-type", "application/json"); + + // This function will find matching parameters in both json objects and populate the matches + // list with the key and the value (response: ApiResponse, nodeData: The model's node data returned by our database) + function findPropertyMatches(response, nodeData) { + // Extract key names and values from the first JSON object + const keyValues = response.data[0]; + + // Initialize an array to store matches with values + const matches = []; + + // Iterate over the 'data' array in the second JSON object + nodeData.nodes.forEach((node) => { + node.data.forEach((dataItem) => { + // Check if the 'value' key exists + if (dataItem.hasOwnProperty("value")) { + // Check if the value matches any key names from the first JSON object + const keyName = dataItem.value; + if (keyValues.hasOwnProperty(keyName)) { + matches.push({ key: keyName, value: keyValues[keyName] }); + } + } + }); + }); + console.log(matches); + return matches; + } + + const response = fetch("https://api.realestateapi.com/v2/PropGPT", { + method: "POST", + headers: requestHeaders, + body: JSON.stringify({ + size: 25, + query: input.userInput, + }), + }) + .then((response) => response.json()) + .then((response) => { + /* Do something with the response */ + console.log(response); + return findPropertyMatches(response, nodeData); + }) + .catch((err) => console.error(err)); + }), +});