Faaala pessoas, amáveis!
Meu nome é Kevin Uehara, Staff Frontend Engineer no IFood, palestrante e criador de conteúdo.
Nesse artigo quero apresentar uma aplicação pessoal desenvolvida, utilizando mapas e realizando integração com o openAPI para geração de localizações. Imagine uma aplicação que pode roterizar os seus destinhos ou lugares mais conhecidos em determinada localização.
Trabalhar com mapas no frontend era algo que nunca tinha trabalhado em particular e achei que era algo muito mais complexo (ainda penso assim, mas muito menos). Lidar com mapas, renderização de polígonos, dados espaciais sendo todos processados no client era algo que eu ficava me perguntando: como isso foi feito? Magia? Não! É tecnologia...
Talvez valha a pena falar sobre os desafios que enfrentei para criar as soluções que temos no frontend do iFood, em outro artigo. Mas aqui quero ser muito mais prático e prático na construção de um aplicativo de demonstração.
A ideia principal é apresentar o aplicativo e oferecer uma visão geral das ferramentas para construção de um aplicativo utilizando mapas. Mostrarei como integrar um mapa ao frontend e com base no que o usuário escrever em um campo textual, seja uma dúvida geográfica, o openAI irá sugerir localizações e será possível interagir com o mapa.
O objetivo é trazer algumas das ferramentas que tratam de mapas e são utilizadas no mercado. A aplicação foi construída utilizando a biblioteca Vite + React como ferramenta de front-end, Typescript, MapLibre, Tailwind e o openAI.
Introdução
Como mencionei anteriormente (spoilers) vou usar o React com Typescript como boilerplate frontend, além de usar o Vite como ferramenta para gerenciar pacotes/dependências e criar o projeto. Por si só, o Vite (usar rollup) já valeria outro artigo falando apenas sobre ele, mas para não fugir do objetivo deste artigo, ao final disponibilizarei links para cada documentação. Então, ao invés de usar o Create React App (CRA) estarei usando o Vite, que vai nos trazer tudo que precisamos, agilizar, estruturar sua arquitetura enxuta.
Para facilitar nossa vida, também utilizarei o Tailwind para estilizar nossa aplicação, trazendo nossos estilos de forma simples e fácil de aplicar.
Também usarei a biblioteca de open source Maplibre para renderização de mapas. O React Map GL que nos fornecerá diversos componentes React focados em interações no mapa. Além disso, usarei MapTiler como estilo de mapa. MapTiler nos proporcionará um mapa mais bonito e limpo, sendo gratuito até um limite de solicitações. Por se tratar de um aplicativo de demonstração e exemplo, não nos preocuparemos com isso, mas fique atento a esse ponto (lembrando que existem estilos de mapas de código aberto do Open Street Maps, comumente conhecido como OSM, que você pode usar).
Em resumo usaremos:
Vite (Boilerplate)
React + Typescript
Tailwind (framework CSS)
MapLibre (Lib para renderizar o mapa)
OpenAI - Ferramenta de IA
Basicamente o que iremos criar é uma applicação web:
Mais
Uma aplicação que utiliza IA (sem o will smith infelizmente) com mapas.
Tecnologias
OpenAI
O OpenAI em si é um laboratório de pesquisas relacionadas à inteligência artificial no EUA. Sendo que possui uma série de produtos e API's, sendo o mais conhecido o ChatGPT.
Iremos utilizar uma de suas API's através de um modelo de IA. Possui uma cota gratuita de 5 doláres, porém para essa demos precisei utilizar e pagar um custo um pouco maior.
Após a criação da sua conta, também poderá utilizar o playground do openAI:
MapLibre
Biblioteca open source para publicar mapas em sua aplicação. A exibição otimizada é possível graças à renderização de bloco vetorial acelerado por GPU e WebGL.
MapTiler
O MapTiler é um provedor de mapas com estilos e customizável. Podemos criar uma conta gratuita, porém possui cotas nas requisições, se tornando pago para sua utilização.
O MapTiler não é obrigatório para essa aplicação, sendo outra opção é utilizar o OpenStreetMaps (OSM) que é um provedor de mapeamento Open Source.
React Map GL
Biblioteca de componentes integrados ao MapLibre/Mapbox para se utilizar na sua aplicação. Permite utilizar hooks próprios para se integrar aos componentes e ao mapa. Facilitando assim, nossa vida como desenvolvedores :)
Show me the code
Vamos criar nossa aplicação utilizando o vite, com react e typescript com o comando:
yarn create vite openai-maps --template react-ts
Entrando no projeto criado e instalando as dependências:
yarn
E rodando nossa aplicação:
yarn dev
Temos nossa tela inicial e projeto iniciado:
Logo após, vamos configurar o tailwind, seguindo a própria documentação:
O próximo passo é criar e mapear as API Key's em um arquivo .env
Lembrando que você irá precisar criar uma conta no OpenAI e no MapTiler para obters as api keys.
Em seguida, iremos instalar as dependências necessárias:
yarn add maplibre-gl react-map-gl openai
Alterando nosso App.tsx
para utilizar o Mapa do MapLibre e os estilos do MapTiler:
JÁ TEMOS UMA MAPA NA NOSSA APLICAÇÃO:
Agora, iremos criar 3 componentes: LoadingSpinner, NavigationLabel e Sideber:
Irei criar dentro do diretório src/components
LoadingSpinner
Apenas um componente Loading:
export const LoadingSpinner = () => {
return (
<div className="flex flex-col items-center">
<div
className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-blue-600 border-solid border-current border-r-transparent align-[-0.125em] text-info motion-reduce:animate-[spin_1.5s_linear_infinite]"
role="status"
></div>
<label className="mt-2 text-base text-blue-700">Carregando...</label>
</div>
);
};
SideBar
Componente responsável por ser um sidebar lateral onde vai conter o campo de busca e os resultados obtidos do openAI:
import { PropsWithChildren } from "react";
interface SideBarProps {
isOpen: boolean;
handleOpen: (isOpen: boolean) => void;
}
export const SideBar: React.FC<PropsWithChildren<SideBarProps>> = ({
children,
isOpen,
handleOpen,
}) => {
return (
<div className="absolute z-10">
<div className="bg-white p-6 relative m-[14px] rounded-sm">
<button
onClick={() => handleOpen(!isOpen)}
className="bg-hamburger w-[28px] h-[28px] absolute text-4xl left-2 top-1"
id="button_aside"
>
≡
</button>
</div>
{isOpen && (
<aside
className="grid fixed top-0 bg-[#F6F9FE] w-3/12 h-full left-0 ease-out delay-150 duration-300 rounded-r-[25px] rounded-bl-[25px]"
id="aside"
>
<div className="flex justify-between mt-8">
<h1 className="text-[#6164E8] font-bold text-[13px] text-center text-xl ml-5">
OpenAI
</h1>
<i
className="mr-8 hover:text-red-600 hover:cursor-pointer text-xl"
onClick={() => handleOpen(false)}
>
X
</i>
</div>
<div className="flex flex-col h-screen ml-5 mt-3">{children}</div>
</aside>
)}
</div>
);
};
NavigationLabel
Componente responsável por pegar a localização gerada pelo openAI e após selecionado, utiliza o hook do react-map-gl para ir até o local (utilizando a função flyTo())
import { useMap } from "react-map-gl/maplibre";
import { MessageResult } from "../../App";
interface NavigateLabelProps extends MessageResult {
index: number;
handleLocation: (location: number[]) => void;
}
export const NavigateLabel = ({
location,
name,
index,
handleLocation,
}: NavigateLabelProps) => {
const { current: map } = useMap();
const onClick = () => {
if (location) {
handleLocation(location);
const [lat, lng] = location;
map?.flyTo({ center: [lng, lat], zoom: 14 });
return;
}
map?.flyTo({ center: [-46.6388, -23.5489], zoom: 14 });
};
return (
<label
onClick={onClick}
key={index}
className="text-blue-700 hover:cursor-pointer hover:text-blue-500"
>
{index + 1} - {name}
</label>
);
};
Agora vamos criar nossa integração com o OpenAI, criando um arquivo Service.
import { Configuration, OpenAIApi } from "openai";
const configuration = new Configuration({
apiKey: import.meta.env.VITE_OPENAI_API_KEY,
});
const MESSAGE_COMPLEMENT =
"formatado em json array com o campo name, representando o nome do lugar e a localização. E o campo location sendo um array com as coordenadas";
export class OpenAi {
static async getLocations(message: string) {
const OpenAi = new OpenAIApi(configuration);
try {
const response = await OpenAi.createCompletion({
model: "text-davinci-003",
prompt: `${message} ${MESSAGE_COMPLEMENT}`,
max_tokens: 500,
temperature: 0,
});
return response;
} catch (error) {
console.error(error);
}
}
}
E A MAGIA ESTÁ AÍ!
Eu simulo a resposta do openAI como se fosse uma chamada à uma API formatada em JSON.
Graças à constante:
const MESSAGE_COMPLEMENT =
"formatado em json array com o campo name, representando o nome do lugar e a localização. E o campo location sendo um array com as coordenadas";
Assim eu concateno o que o usuário digitou com o formato que eu espero, para assim, renderizar de forma correta os componentes.
Visualizando no playground do openAI:
Agora refatorando nosso App.tsx
por todos os componentes e chamando nosso Service.
import Map, { Marker } from "react-map-gl/maplibre";
import { OpenAi } from "./services/openai";
import "maplibre-gl/dist/maplibre-gl.css";
import { SideBar } from "./components/Sidebar";
import { useState } from "react";
import { PinIcon } from "./components/icons";
import { LoadingSpinner } from "./components/LoadingSpinner";
import { NavigateLabel } from "./components/NavigateLabel";
export interface MessageResult {
name?: string;
location?: number[];
}
const initialValueLocation = {
longitude: -47.0616,
latitude: -22.9064,
zoom: 10,
};
function App() {
const [isSideBarOpen, setIsSideBarOpen] = useState(false);
const [inputValue, setInputValue] = useState("");
const [messageResult, setMessageResult] = useState<MessageResult[]>([]);
const [chosenLocation, setChosenLocation] = useState<number[]>([]);
const [isLoading, setIsLoading] = useState(false);
const handleMessage = async () => {
setIsLoading(true);
const response = await OpenAi.getLocations(inputValue);
const message = response?.data.choices.length
? response?.data.choices[0].text
: "";
try {
if (message) {
setMessageResult(JSON.parse(message));
}
} catch (error) {
console.error("Failed on JSON Parse");
} finally {
setIsLoading(false);
}
};
const handleCleanResults = () => {
setInputValue("");
setMessageResult([]);
setChosenLocation([]);
};
return (
<>
<div className="relative top-0 left-0">
<Map
initialViewState={initialValueLocation}
style={{ width: "100vw", height: "100vh" }}
mapStyle={`https://api.maptiler.com/maps/cadastre-satellite/style.json?key=${
import.meta.env.VITE_MAP_TILER_API_KEY
}`}
>
<SideBar
isOpen={isSideBarOpen}
handleOpen={() => setIsSideBarOpen(!isSideBarOpen)}
>
<label className="text-base mt-3 mb-2" htmlFor="message">
O que você gostaria de saber?
</label>
<input
type="text"
name="message"
id="message"
value={inputValue}
onChange={(evt) => setInputValue(evt.target.value)}
className="w-11/12 pr-2 pl-2 rounded-sm h-14 border-blue-800 text-base border-2"
/>
<span className="text-sm text-gray-500 mt-1">
Ex: Me dê sugestões de praias no brasil
</span>
<button
className="w-11/12 h-12 mt-3 bg-blue-800 hover:bg-blue-500 text-white rounded text-base"
onClick={handleMessage}
>
Buscar
</button>
<button
className="w-11/12 h-12 mt-2 bg-red-700 hover:bg-red-500 text-white rounded text-base"
onClick={handleCleanResults}
>
Limpar Busca
</button>
{isLoading && (
<div className="mt-3 flex justify-center">
<LoadingSpinner />
</div>
)}
<div className="mt-10 text-base flex flex-col">
{!isLoading && messageResult.length > 0 && (
<h2 className="text-xl text-blue-700 font-bold mb-2">
Resultados
</h2>
)}
{!isLoading &&
messageResult.map((result, index) => (
<NavigateLabel
{...result}
index={index}
handleLocation={(loc) => setChosenLocation(loc)}
/>
))}
</div>
<div className="mt-20">
<label className="text-sm text-gray-500">
Created by Kevin Uehara
</label>
</div>
</SideBar>
{chosenLocation && chosenLocation.length && (
<Marker latitude={chosenLocation[0]} longitude={chosenLocation[1]}>
<PinIcon />
</Marker>
)}
</Map>
</div>
</>
);
}
export default App;
Lembrando que não estou utilizando nenhum gerenciador de estado como ContextAPI, Redux, Jotai, Zustand etc... Apenas estou utilizando o próprio state e passando via prop-drilling.
FINALMENTE TEMOS NOSSA APLICAÇÃO:
E finalmente finalizamos nossa aplicação, utilizando IA para gerar localizações em mapas. Incrível, não?
Em resumo é isso
Essa mesma apresentação que se encontra nesse artigo foi feita através de uma live no canal da NodeBR:
[▶️LIVE NODEBR] - Integrando IA com Mapas para geração de localizações
Então por hoje é isso galera!
Muito obrigado e fiquem bem sempre!
NodeBR Linktree: https://linktr.ee/nodebr
Contatos:
Youtube: https://www.youtube.com/@ueharakevin/
Linkedin: https://www.linkedin.com/in/kevin-uehara
Instagram: https://www.instagram.com/uehara_kevin/
Twitter: https://twitter.com/ueharaDev
Github: https://github.com/kevinuehara
dev.to: https://dev.to/kevin-uehara
Email: uehara.kevin@gmail.com
Top comments (3)
Ficou muito bom cara, curti demais!
Aeeeew demais! Primeiro artigo com estilo!
Show de bola Kevin! Muito bom!