From 1cce215e998da81acc1173d04198a288c0c5d87a Mon Sep 17 00:00:00 2001 From: nsbradford Date: Wed, 24 Jan 2024 19:00:31 -0800 Subject: [PATCH] refactor fill --- .devcontainer/devcontainer.json | 11 -- .github/workflows/playwright.yml | 5 +- devcontainer.Dockerfile | 12 -- package.json | 1 + src/components/CreateFormInner.tsx | 38 ++++ src/components/ErrorBox.tsx | 12 ++ .../[id].tsx => components/InnerChat.tsx} | 168 ++++-------------- src/components/MiniSpinner.tsx | 10 ++ src/components/SubmissionBox.tsx | 15 ++ src/components/auth/ErrorAlert.tsx | 2 +- src/components/auth/SignInForm.tsx | 4 +- src/components/auth/SignUpForm.tsx | 4 +- src/components/chat.tsx | 2 +- src/components/landing/Team.tsx | 1 - src/components/layout/Page.tsx | 4 +- src/components/misc.tsx | 4 - .../{home => settings}/DashboardMode.tsx | 6 +- .../{home => settings}/ErrorMode.tsx | 0 src/components/{home => settings}/NavBar.tsx | 2 +- src/components/{home => settings}/Spinner.tsx | 0 .../modes/ResponsesTable.tsx | 0 src/{types.ts => models.ts} | 0 src/pages/api/llm.ts | 2 +- src/pages/auth.tsx | 2 +- src/pages/forms/[id].tsx | 38 ++-- src/pages/forms/entry/[id].tsx | 32 ++++ src/pages/forms/new.tsx | 4 +- src/pages/home.tsx | 4 +- src/pages/index.tsx | 4 +- src/pages/settings.tsx | 4 +- src/prompts.ts | 2 +- src/utils.ts | 2 +- tests/form-fill.spec.ts | 25 +-- yarn.lock | 5 + 34 files changed, 197 insertions(+), 228 deletions(-) create mode 100644 src/components/CreateFormInner.tsx create mode 100644 src/components/ErrorBox.tsx rename src/{pages/forms/fill/[id].tsx => components/InnerChat.tsx} (51%) create mode 100644 src/components/MiniSpinner.tsx create mode 100644 src/components/SubmissionBox.tsx rename src/components/{home => settings}/DashboardMode.tsx (97%) rename src/components/{home => settings}/ErrorMode.tsx (100%) rename src/components/{home => settings}/NavBar.tsx (99%) rename src/components/{home => settings}/Spinner.tsx (100%) rename src/components/{home => settings}/modes/ResponsesTable.tsx (100%) rename src/{types.ts => models.ts} (100%) create mode 100644 src/pages/forms/entry/[id].tsx diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 970dcc9..6d8dba8 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,19 +1,14 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node { "name": "TalkForm", "image": "mcr.microsoft.com/devcontainers/javascript-node:0-18-bullseye", "remoteUser": "root", "containerUser": "root", - // https://community.doppler.com/t/vscode-container-support/104 - // https://community.doppler.com/t/doppler-and-github-codespaces/989/2 "containerEnv": { "DOPPLER_TOKEN": "${localEnv:DOPPLER_CLI_TOKEN}" }, - // Features to add to the dev container. More info: https://containers.dev/features. "features": { "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {}, @@ -21,7 +16,6 @@ "ghcr.io/metcalfc/devcontainer-features/doppler:0.1.1": {} }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [ 3000, 65455 // Required for Ellipsis @@ -36,13 +30,8 @@ "requireLocalPort": true } }, - "postStartCommand": "nohup bash -c 'ellipsis listener start . &'", // This allows Ellipsis to write file changes in the codespace. - - // TODO can't get the yarn install within Dockerfile to show up in dev container. - // This also seems to be what the Microsoft example devcontainers do. "postCreateCommand": "yarn install --frozen-lockfile", - // Configure tool-specific properties. "customizations": { "vscode": { "settings": {}, diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index f83b5ec..3216ce7 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -25,7 +25,6 @@ jobs: with: node-version: 18 - name: Install dependencies - # run: npm ci - https://stackoverflow.com/questions/58482655/what-is-the-closest-to-npm-ci-in-yarn run: yarn install --frozen-lockfile - name: Install Playwright Browsers run: npx playwright install --with-deps @@ -40,8 +39,7 @@ jobs: path: playwright-report/ retention-days: 30 - # There isn't an easy way to load the devcontainer itself, - # so we make do with just the dockerfile + # TODO use devcontainer CLI build_devcontainer: name: Build devcontainer runs-on: ubuntu-latest @@ -52,5 +50,4 @@ jobs: - name: Build container run: | docker build -t devcontainer-instance . - # docker run devcontainer diff --git a/devcontainer.Dockerfile b/devcontainer.Dockerfile index 41f5221..0f85a26 100644 --- a/devcontainer.Dockerfile +++ b/devcontainer.Dockerfile @@ -1,11 +1,6 @@ -# Use the specified image -# this had trouble with the `yarn install` -# FROM mcr.microsoft.com/devcontainers/typescript-node:1-18-bullseye -# FROM node:18.15.0-alpine FROM node:18.15.0-bullseye-slim # codespaces automatically clones the repo into /workspaces/ -# WORKDIR /app # necessary for some of the npm packages RUN apt-get update && apt-get install -y \ @@ -20,14 +15,7 @@ RUN apt-get update && apt-get install -y apt-transport-https ca-certificates cur apt-get update && \ apt-get -y install doppler -# codespaces automatically clones the repo into /workspaces/ -# COPY package.json yarn.lock ./ -# codespaces automatically clones the repo into /workspaces/ RUN yarn install --frozen-lockfile -# Copy the rest of the project files into the working directory -# COPY . . - -# Expose the port your app runs on EXPOSE 3000 \ No newline at end of file diff --git a/package.json b/package.json index f6cdcca..a384c90 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "openai": "^4.3.1", "postcss": "8.4.28", "posthog-js": "^1.77.2", + "promptlayer": "^0.0.7", "react": "18.2.0", "react-dom": "18.2.0", "tailwindcss": "3.3.3", diff --git a/src/components/CreateFormInner.tsx b/src/components/CreateFormInner.tsx new file mode 100644 index 0000000..20497b9 --- /dev/null +++ b/src/components/CreateFormInner.tsx @@ -0,0 +1,38 @@ +import { Form } from '@/models'; +import { getFormFromSupabase } from '@/utils'; +import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; +import React, { useEffect, useState } from 'react'; +import { Database } from '../../types/supabase'; +import { ErrorBox } from './ErrorBox'; +import { InnerChat } from '@/components/InnerChat'; + + +export function CreateFormInner(props: { formId: string; }) { + const { formId } = props; + const supabase = createClientComponentClient(); + const [form, setForm] = useState
(null); + const [error, setError] = useState(null); + useEffect(() => { + if (!form) { + getFormFromSupabase(formId, supabase).then((maybeForm) => { + if (maybeForm instanceof Error) { + console.error(maybeForm.message); + setError(maybeForm); + } else { + setForm(maybeForm); + } + }); + } + }, []); // The empty array ensures this effect runs only once on mount + return form ? ( + + ) : ( + <> + {error ? ( + ErrorBox(error) + ) : ( +

Loading...

+ )} + + ); +} diff --git a/src/components/ErrorBox.tsx b/src/components/ErrorBox.tsx new file mode 100644 index 0000000..1b1a042 --- /dev/null +++ b/src/components/ErrorBox.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +export function ErrorBox( + error: Error +): React.ReactNode { + return ( +
+

Error

+

{error.message}

+
+ ); +} diff --git a/src/pages/forms/fill/[id].tsx b/src/components/InnerChat.tsx similarity index 51% rename from src/pages/forms/fill/[id].tsx rename to src/components/InnerChat.tsx index c763e35..850ca65 100644 --- a/src/pages/forms/fill/[id].tsx +++ b/src/components/InnerChat.tsx @@ -1,81 +1,25 @@ import { MessageUI } from '@/components/chat'; -import NavBar from '@/components/home/NavBar'; -import { workSans } from '@/components/misc'; import { PROMPT_FILL } from '@/prompts'; -import { ChatMessage, Form } from '@/types'; +import { ChatMessage, Form } from '@/models'; import { callLLM, - getFormFromSupabase, - submitResponseToSupabase, + submitResponseToSupabase } from '@/utils'; import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { - SupabaseClient, - createClientComponentClient, -} from '@supabase/auth-helpers-nextjs'; -import { useRouter } from 'next/router'; +import { SupabaseClient } from '@supabase/auth-helpers-nextjs'; import React, { useEffect, useRef, useState } from 'react'; -import { Database } from '../../../../types/supabase'; +import { Database } from '../../types/supabase'; +import { SubmissionBox } from './SubmissionBox'; +import { ErrorBox } from './ErrorBox'; +import { MiniSpinner } from './MiniSpinner'; -// Makes it much easier to track renders/fetches by wrapping the component. -export default function CreateForm() { - const router = useRouter(); - // If the page is still loading (especially during ISR or fallback scenarios), show a loading state - const formId = router.query.id as string; - return ( - <> -
- -
- {router.isFallback || typeof formId !== 'string' ? ( -

Loading...

- ) : ( - - )} -
-
- - ); -} - -export function CreateFormInner(props: { formId: string }) { - const { formId } = props; - const supabase = createClientComponentClient(); - const [form, setForm] = useState(null); - const [error, setError] = useState(null); - useEffect(() => { - if (!form) { - getFormFromSupabase(formId, supabase).then((maybeForm) => { - if (maybeForm instanceof Error) { - console.error(maybeForm.message); - setError(maybeForm); - } else { - setForm(maybeForm); - } - }); - } - }, []); // The empty array ensures this effect runs only once on mount - return form ? ( - - ) : ( - <> - {error ? ( - ErrorBox(error) - ) : ( -

Loading...

- )} - - ); -} -export function InnerChat(props: { - form: Form; - supabase: SupabaseClient; -}) { +/** + * Main form-filling chat UI + * @param props + * @returns + */ +export function InnerChat(props: { form: Form; supabase: SupabaseClient;}) { const { form } = props; const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(''); @@ -85,17 +29,29 @@ export function InnerChat(props: { const [submission, setSubmission] = useState(null); const [error, setError] = useState(null); + useEffect(() => { + if (!isWaiting && inputRef.current) { + inputRef.current.focus(); + } + }, [isWaiting]); + + useEffect(() => { + if (messages.length === 0) { + handleSubmit(); + } + }, []); + + const handleSubmit = async (userMessage?: string) => { - const messagesToSend = - userMessage && userMessage.trim() - ? [ - ...messages, - { - role: 'user' as const, - content: `{ "user_message": "${userMessage.trim()}" }`, // extra JSON to keep model behaving - }, - ] - : messages; + const messagesToSend = userMessage && userMessage.trim() + ? [ + ...messages, + { + role: 'user' as const, + content: `{ "user_message": "${userMessage.trim()}" }`, + }, + ] + : messages; setMessages(messagesToSend); setInputValue(''); setIsWaiting(true); @@ -147,18 +103,6 @@ export function InnerChat(props: { handleSubmit(inputValue); } }; - useEffect(() => { - if (!isWaiting && inputRef.current) { - // Ensure the input gets focus when isWaiting transitions to false - inputRef.current.focus(); - } - }, [isWaiting]); // Track changes to the isWaiting state - - useEffect(() => { - if (messages.length === 0) { - handleSubmit(); - } - }, []); // The empty array ensures this effect runs only once on mount return ( <> @@ -172,8 +116,7 @@ export function InnerChat(props: { + content={message.content} /> ))}
setInputValue(e.target.value)} disabled={isWaiting || isDone} onKeyPress={handleKeyPress} - ref={inputRef} - /> + ref={inputRef} /> diff --git a/src/components/home/ErrorMode.tsx b/src/components/settings/ErrorMode.tsx similarity index 100% rename from src/components/home/ErrorMode.tsx rename to src/components/settings/ErrorMode.tsx diff --git a/src/components/home/NavBar.tsx b/src/components/settings/NavBar.tsx similarity index 99% rename from src/components/home/NavBar.tsx rename to src/components/settings/NavBar.tsx index ac36cf4..f97fab8 100644 --- a/src/components/home/NavBar.tsx +++ b/src/components/settings/NavBar.tsx @@ -9,7 +9,7 @@ import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; import { useSessionContext } from '@supabase/auth-helpers-react'; import { useRouter } from 'next/router'; import { Database } from '../../../types/supabase'; -import { User } from '@/types'; +import { User } from '@/models'; import Link from 'next/link'; function classNames(...classes: string[]) { diff --git a/src/components/home/Spinner.tsx b/src/components/settings/Spinner.tsx similarity index 100% rename from src/components/home/Spinner.tsx rename to src/components/settings/Spinner.tsx diff --git a/src/components/home/modes/ResponsesTable.tsx b/src/components/settings/modes/ResponsesTable.tsx similarity index 100% rename from src/components/home/modes/ResponsesTable.tsx rename to src/components/settings/modes/ResponsesTable.tsx diff --git a/src/types.ts b/src/models.ts similarity index 100% rename from src/types.ts rename to src/models.ts diff --git a/src/pages/api/llm.ts b/src/pages/api/llm.ts index 8858f08..3083688 100644 --- a/src/pages/api/llm.ts +++ b/src/pages/api/llm.ts @@ -1,7 +1,7 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type { NextApiRequest, NextApiResponse } from 'next'; import OpenAI from 'openai'; -import { LLMRequest, LLMResponse } from '../../types'; +import { LLMRequest, LLMResponse } from '../../models'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, diff --git a/src/pages/auth.tsx b/src/pages/auth.tsx index b22feb0..81ccca1 100644 --- a/src/pages/auth.tsx +++ b/src/pages/auth.tsx @@ -1,6 +1,6 @@ 'use client'; import SignInForm from '@/components/auth/SignInForm'; -import NavBar from '@/components/home/NavBar'; +import NavBar from '@/components/settings/NavBar'; import { useSessionContext } from '@supabase/auth-helpers-react'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; diff --git a/src/pages/forms/[id].tsx b/src/pages/forms/[id].tsx index 283abdb..75f32ae 100644 --- a/src/pages/forms/[id].tsx +++ b/src/pages/forms/[id].tsx @@ -1,6 +1,6 @@ -import { SpinnerFullPage } from '@/components/home/Spinner'; +import { SpinnerFullPage } from '@/components/settings/Spinner'; import Page from '@/components/layout/Page'; -import { Form, Response, User } from '@/types'; +import { Form, Response, User } from '@/models'; import { getFormFromSupabase, getResponsesFromSupabase, @@ -12,25 +12,18 @@ import Link from 'next/link'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { Database } from '../../../types/supabase'; -import ResponsesTable from '../../components/home/modes/ResponsesTable'; +import ResponsesTable from '../../components/settings/modes/ResponsesTable'; export default function FormDetailPage() { + const [responses, setResponses] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const { push } = useRouter(); + const { isLoading: isSessionLoading, session, error } = useSessionContext(); const supabase = createClientComponentClient(); const [user, setUser] = useState(null); const [form, setForm] = useState(null); - const [responses, setResponses] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const { push } = useRouter(); - useEffect(() => { - if (!isSessionLoading && !session) { - push('/auth'); - } - if (!isSessionLoading && session) { - getUserFromSupabase(session, supabase, setUser); - } - }, [isLoading, session]); useEffect(() => { const getFormAndResponses = async () => { @@ -52,15 +45,26 @@ export default function FormDetailPage() { } }, [isSessionLoading, user, form, responses]); + useEffect(() => { + if (!isSessionLoading && !session) { + push('/auth'); + } + if (!isSessionLoading && session) { + getUserFromSupabase(session, supabase, setUser); + } + }, [isLoading, session]); + if (isLoading || isSessionLoading || form === null || responses === null) { return ; } const badgeColor = form.is_open - ? 'bg-green-100 text-green-800' - : 'bg-red-100 text-red-800'; + ? 'bg-green-100 text-green-600' + : 'bg-red-100 text-red-600'; + const camelCaseTitle = form.name.charAt(0).toUpperCase() + form.name.slice(1, form.name.length); + return (
@@ -78,7 +82,7 @@ export default function FormDetailPage() {
- + diff --git a/src/pages/forms/entry/[id].tsx b/src/pages/forms/entry/[id].tsx new file mode 100644 index 0000000..6711e0d --- /dev/null +++ b/src/pages/forms/entry/[id].tsx @@ -0,0 +1,32 @@ +import NavBar from '@/components/settings/NavBar'; +import { workSans } from '@/components/misc'; +import { useRouter } from 'next/router'; +import React from 'react'; +import { CreateFormInner } from '../../../components/CreateFormInner'; + +// Makes it much easier to track renders/fetches by wrapping the component. +export default function CreateForm() { + const router = useRouter(); + // If the page is still loading (especially during ISR or fallback scenarios), show a loading state + const formId = router.query.id as string; + return ( + <> +
+ +
+ {router.isFallback || typeof formId !== 'string' ? ( +

Loading...

+ ) : ( + + )} +
+
+ + ); +} + + diff --git a/src/pages/forms/new.tsx b/src/pages/forms/new.tsx index d8c7b71..2ba9c02 100644 --- a/src/pages/forms/new.tsx +++ b/src/pages/forms/new.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { Database } from '../../../types/supabase'; import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; -import { ChatMessage, User } from '@/types'; +import { ChatMessage, User } from '@/models'; import { v4 } from 'uuid'; import { callLLM, getUserFromSupabase } from '@/utils'; import { PROMPT_BUILD } from '@/prompts'; @@ -10,7 +10,7 @@ import Page from '@/components/layout/Page'; import { useRouter } from 'next/router'; import { useSessionContext } from '@supabase/auth-helpers-react'; import Link from 'next/link'; -import { Spinner } from '@/components/home/Spinner'; +import { Spinner } from '@/components/settings/Spinner'; type NewFormPageProps = { user: User; diff --git a/src/pages/home.tsx b/src/pages/home.tsx index b3574cd..25a60fd 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -1,12 +1,12 @@ import Page from '@/components/layout/Page'; -import DashboardMode from '@/components/home/DashboardMode'; +import DashboardMode from '@/components/settings/DashboardMode'; import { getUserFromSupabase } from '@/utils'; import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; import { useSessionContext } from '@supabase/auth-helpers-react'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { Database } from '../../types/supabase'; -import { User } from '@/types'; +import { User } from '@/models'; export default function HomePage() { const { push } = useRouter(); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 6148ca9..11832c6 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,4 +1,4 @@ -import NavBar from '@/components/home/NavBar'; +import NavBar from '@/components/settings/NavBar'; import { ChatHistory, FloatingTextBox, @@ -59,7 +59,7 @@ function GetStartedButtons() { View on GitHub Fill a sample form diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 630a403..d98edf0 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -1,11 +1,11 @@ -import { User } from '@/types'; +import { User } from '@/models'; import { getUserFromSupabase } from '@/utils'; import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; import { useSessionContext } from '@supabase/auth-helpers-react'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { Database } from '../../types/supabase'; -import { SpinnerFullPage } from '@/components/home/Spinner'; +import { SpinnerFullPage } from '@/components/settings/Spinner'; import Page from '@/components/layout/Page'; export default function SettingsMode() { diff --git a/src/prompts.ts b/src/prompts.ts index 199356a..a3009fa 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -1,4 +1,4 @@ -import { Form } from '@/types'; +import { Form } from '@/models'; export const PROMPT_BUILD = `You are TalkForm AI, a helpful, honest, and harmless AI assistant helping gather information from users and using it to populate forms. First, we need to create a form.`; diff --git a/src/utils.ts b/src/utils.ts index 0b5d4c3..7bdbacd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,7 +5,7 @@ import { LLMResponse, Response, User, -} from '@/types'; +} from '@/models'; import { Session, SupabaseClient } from '@supabase/auth-helpers-nextjs'; import { v4 } from 'uuid'; import { Database, Json } from '../types/supabase'; diff --git a/tests/form-fill.spec.ts b/tests/form-fill.spec.ts index 03ad309..a26e874 100644 --- a/tests/form-fill.spec.ts +++ b/tests/form-fill.spec.ts @@ -7,7 +7,7 @@ async function runTestScenario( expected: object ) { // Navigate to the specified URL - await page.goto(`http://localhost:3000/forms/fill/${formId}`); + await page.goto(`http://localhost:3000/forms/entry/${formId}`); for (const message of messages) { // Wait for input to be enabled and visible @@ -53,31 +53,8 @@ const testScenarios = [ github: 'github.com/jd123456', }, }, - // TODO failing: LLM stopped giving structured responses and/or cache not working - // { - // name: 'public-facing demo', - // formId: '5771953d-a003-4969-9071-fcfff4c5bb10', - // messages: [ - // 'Nick', - // 'e@e.co', - // 'big tech co', - // 'eng manager', - // 'https://github.com/nsbradford', - // 'google analytics and Posthog', - // 'confirm', - // ], - // expected: { - // name: 'Nick', - // email_address: 'e@e.co', - // company: 'big tech co', - // job_title: 'eng manager', - // github_username: 'nsbradford', - // technologies_used: 'google analytics, Posthog', - // }, - // }, ]; -// Iterate through your scenarios to run the tests for (const scenario of testScenarios) { test(`Chat component e2e test: ${scenario.name}`, async ({ page }) => { await runTestScenario( diff --git a/yarn.lock b/yarn.lock index b265b2f..e3798da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2463,6 +2463,11 @@ prettier@^3.0.3: resolved "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz" integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== +promptlayer@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/promptlayer/-/promptlayer-0.0.7.tgz#1cfb76db24464bc3e3d52d8d1e9a01f49a9cbf73" + integrity sha512-tuJCNW72XpG79GzU/kODuLpfSOOoDJ3XE7cmgFdWYBLIuCvNdPV1PxOh1bi9jFWqVFYR3kUUSjtucB7bfAXpDQ== + prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"