From e6232854523a4d88fbbd664f944fb593e8cbe2aa Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Fri, 2 Aug 2024 17:53:51 -0400 Subject: [PATCH] feat: chatbot component (#1209) Co-authored-by: Rohin Bhargava Co-authored-by: Rohin Bhargava Co-authored-by: Rohin Bhargava --- .github/workflows/fdr-ete.yml | 6 + packages/commons/search-utils/package.json | 1 + .../src/getSlugForSearchRecord.ts} | 27 +- packages/commons/search-utils/src/index.ts | 2 + .../search-utils/src}/types.ts | 0 packages/ui/app/package.json | 7 +- packages/ui/app/src/atoms/cohere.ts | 3 + packages/ui/app/src/atoms/index.ts | 1 + packages/ui/app/src/css/globals.scss | 1 + packages/ui/app/src/hooks/useRouteChanged.ts | 15 + packages/ui/app/src/search/SearchDialog.tsx | 26 +- packages/ui/app/src/search/SearchHit.tsx | 3 +- packages/ui/app/src/search/SearchHits.tsx | 3 +- .../search/algolia/AlgoliaSearchDialog.tsx | 16 +- .../src/search/cohere/CohereChatButton.tsx | 126 + .../app/src/search/content/EndpointRecord.tsx | 2 +- .../src/search/content/EndpointRecordV2.tsx | 2 +- .../ui/app/src/search/content/PageRecord.tsx | 2 +- .../app/src/search/content/PageRecordV2.tsx | 2 +- packages/ui/app/tailwind.config.cjs | 3 +- packages/ui/chatbot/.eslintrc.cjs | 11 + packages/ui/chatbot/.gitignore | 24 + packages/ui/chatbot/.prettierrc.cjs | 1 + packages/ui/chatbot/.stylelintrc.json | 1 + packages/ui/chatbot/README.md | 30 + packages/ui/chatbot/index.html | 13 + packages/ui/chatbot/package.json | 56 + packages/ui/chatbot/postcss.config.js | 6 + packages/ui/chatbot/src/App.scss | 59 + packages/ui/chatbot/src/App.tsx | 15 + .../src/atoms/atomWithSessionStorage.ts | 68 + .../ui/chatbot/src/components/AskInput.tsx | 40 + .../src/components/ChatConversation.tsx | 29 + .../chatbot/src/components/ChatbotModal.tsx | 128 + .../ui/chatbot/src/components/FernAvatar.tsx | 10 + .../src/components/MarkdownContent.tsx | 50 + .../src/components/ResponseMessage.tsx | 74 + .../ui/chatbot/src/components/SendButton.tsx | 17 + .../ui/chatbot/src/components/TextArea.tsx | 28 + .../ui/chatbot/src/components/UserMessage.tsx | 12 + packages/ui/chatbot/src/icons/CohereIcon.tsx | 31 + packages/ui/chatbot/src/icons/FernIcon.tsx | 18 + packages/ui/chatbot/src/icons/SendIcon.tsx | 22 + packages/ui/chatbot/src/index.scss | 10 + packages/ui/chatbot/src/index.ts | 3 + packages/ui/chatbot/src/main.tsx | 9 + packages/ui/chatbot/src/types.ts | 24 + packages/ui/chatbot/src/vite-env.d.ts | 1 + packages/ui/chatbot/tailwind.config.js | 54 + packages/ui/chatbot/tsconfig.app.json | 28 + packages/ui/chatbot/tsconfig.json | 11 + packages/ui/chatbot/tsconfig.node.json | 13 + packages/ui/chatbot/vite.config.ts | 7 + packages/ui/docs-bundle/package.json | 11 +- .../src/pages/api/fern-docs/proxy/stream.ts | 1 - .../src/pages/api/fern-docs/search/cohere.ts | 197 + packages/ui/docs-bundle/tailwind.config.js | 3 +- packages/ui/tailwind.config.js | 1 - pnpm-lock.yaml | 3204 +++++++++++++---- servers/fdr/package.json | 2 +- 60 files changed, 3773 insertions(+), 797 deletions(-) rename packages/{ui/app/src/search/util.ts => commons/search-utils/src/getSlugForSearchRecord.ts} (78%) rename packages/{ui/app/src/search => commons/search-utils/src}/types.ts (100%) create mode 100644 packages/ui/app/src/atoms/cohere.ts create mode 100644 packages/ui/app/src/hooks/useRouteChanged.ts create mode 100644 packages/ui/app/src/search/cohere/CohereChatButton.tsx create mode 100644 packages/ui/chatbot/.eslintrc.cjs create mode 100644 packages/ui/chatbot/.gitignore create mode 100644 packages/ui/chatbot/.prettierrc.cjs create mode 100644 packages/ui/chatbot/.stylelintrc.json create mode 100644 packages/ui/chatbot/README.md create mode 100644 packages/ui/chatbot/index.html create mode 100644 packages/ui/chatbot/package.json create mode 100644 packages/ui/chatbot/postcss.config.js create mode 100644 packages/ui/chatbot/src/App.scss create mode 100644 packages/ui/chatbot/src/App.tsx create mode 100644 packages/ui/chatbot/src/atoms/atomWithSessionStorage.ts create mode 100644 packages/ui/chatbot/src/components/AskInput.tsx create mode 100644 packages/ui/chatbot/src/components/ChatConversation.tsx create mode 100644 packages/ui/chatbot/src/components/ChatbotModal.tsx create mode 100644 packages/ui/chatbot/src/components/FernAvatar.tsx create mode 100644 packages/ui/chatbot/src/components/MarkdownContent.tsx create mode 100644 packages/ui/chatbot/src/components/ResponseMessage.tsx create mode 100644 packages/ui/chatbot/src/components/SendButton.tsx create mode 100644 packages/ui/chatbot/src/components/TextArea.tsx create mode 100644 packages/ui/chatbot/src/components/UserMessage.tsx create mode 100644 packages/ui/chatbot/src/icons/CohereIcon.tsx create mode 100644 packages/ui/chatbot/src/icons/FernIcon.tsx create mode 100644 packages/ui/chatbot/src/icons/SendIcon.tsx create mode 100644 packages/ui/chatbot/src/index.scss create mode 100644 packages/ui/chatbot/src/index.ts create mode 100644 packages/ui/chatbot/src/main.tsx create mode 100644 packages/ui/chatbot/src/types.ts create mode 100644 packages/ui/chatbot/src/vite-env.d.ts create mode 100644 packages/ui/chatbot/tailwind.config.js create mode 100644 packages/ui/chatbot/tsconfig.app.json create mode 100644 packages/ui/chatbot/tsconfig.json create mode 100644 packages/ui/chatbot/tsconfig.node.json create mode 100644 packages/ui/chatbot/vite.config.ts create mode 100644 packages/ui/docs-bundle/src/pages/api/fern-docs/search/cohere.ts diff --git a/.github/workflows/fdr-ete.yml b/.github/workflows/fdr-ete.yml index 6ad86b5357..f2edcd18b1 100644 --- a/.github/workflows/fdr-ete.yml +++ b/.github/workflows/fdr-ete.yml @@ -41,5 +41,11 @@ jobs: - name: Install uses: ./.github/actions/install + - name: Install Docker Compose + run: | + sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + docker-compose --version + - name: Run ete test run: pnpm --filter=@fern-platform/fdr test:ete diff --git a/packages/commons/search-utils/package.json b/packages/commons/search-utils/package.json index 713a03bf76..42c85bba81 100644 --- a/packages/commons/search-utils/package.json +++ b/packages/commons/search-utils/package.json @@ -39,6 +39,7 @@ "@types/node": "^18.7.18", "depcheck": "^1.4.3", "eslint": "^8.56.0", + "instantsearch.js": "^4.63.0", "organize-imports-cli": "^0.10.0", "prettier": "^3.3.2", "stylelint": "^16.1.0", diff --git a/packages/ui/app/src/search/util.ts b/packages/commons/search-utils/src/getSlugForSearchRecord.ts similarity index 78% rename from packages/ui/app/src/search/util.ts rename to packages/commons/search-utils/src/getSlugForSearchRecord.ts index 7a3991083f..ad82bdcaaa 100644 --- a/packages/ui/app/src/search/util.ts +++ b/packages/commons/search-utils/src/getSlugForSearchRecord.ts @@ -1,9 +1,7 @@ import { Algolia, FernNavigation } from "@fern-api/fdr-sdk"; -import { SidebarVersionInfo } from "@fern-ui/fdr-utils"; import { UnreachableCaseError } from "ts-essentials"; -import type { SearchRecord } from "./types"; -export function getSlugForSearchRecord(record: SearchRecord, basePath: string | undefined): string { +export function getSlugForSearchRecord(record: Algolia.AlgoliaRecord, basePath: string | undefined): string { return visitSearchRecord(record)._visit({ v3: (record) => record.slug, v2: (record) => @@ -21,6 +19,25 @@ export function getSlugForSearchRecord(record: SearchRecord, basePath: string | }); } +export function getTitleForSearchRecord(record: Algolia.AlgoliaRecord): string { + return visitSearchRecord(record)._visit({ + v3: (record) => record.title, + v2: (record) => + record.type === "endpoint-v2" + ? record.endpoint.path.parts.map((p) => (p.type === "pathParameter" ? `:${p.value}` : p.value)).join("") + : record.title, + v1: (record) => record.title, + }); +} + +export function getContentForSearchRecord(record: Algolia.AlgoliaRecord): string | undefined { + return visitSearchRecord(record)._visit({ + v3: (record) => record.content ?? undefined, + v2: (record) => (record.type === "page-v2" ? record.content : undefined), + v1: () => undefined, + }); +} + function getLeadingPathForSearchRecord(record: Algolia.AlgoliaRecord): string[] { switch (record.type) { case "page": @@ -35,10 +52,10 @@ function getLeadingPathForSearchRecord(record: Algolia.AlgoliaRecord): string[] } export function createSearchPlaceholderWithVersion( - activeVersion: SidebarVersionInfo | undefined, + version: string | undefined, sidebar: FernNavigation.SidebarRootNode | undefined, ): string { - return `Search ${activeVersion != null ? `across ${activeVersion.id} ` : ""}for ${createSearchPlaceholder(sidebar)}...`; + return `Search ${version != null ? `across ${version} ` : ""}for ${createSearchPlaceholder(sidebar)}...`; } function createSearchPlaceholder(sidebar: FernNavigation.SidebarRootNode | undefined): string { diff --git a/packages/commons/search-utils/src/index.ts b/packages/commons/search-utils/src/index.ts index 50b4ab7bce..6c2f838ebe 100644 --- a/packages/commons/search-utils/src/index.ts +++ b/packages/commons/search-utils/src/index.ts @@ -1 +1,3 @@ export * from "./SearchConfig"; +export * from "./getSlugForSearchRecord"; +export * from "./types"; diff --git a/packages/ui/app/src/search/types.ts b/packages/commons/search-utils/src/types.ts similarity index 100% rename from packages/ui/app/src/search/types.ts rename to packages/commons/search-utils/src/types.ts diff --git a/packages/ui/app/package.json b/packages/ui/app/package.json index 90dd84fc9e..18e9c9627c 100644 --- a/packages/ui/app/package.json +++ b/packages/ui/app/package.json @@ -50,13 +50,14 @@ "@emotion/is-prop-valid": "^1.2.2", "@fern-api/fdr-sdk": "workspace:*", "@fern-api/template-resolver": "workspace:*", + "@fern-ui/chatbot": "workspace:*", "@fern-ui/components": "workspace:*", "@fern-ui/core-utils": "workspace:*", "@fern-ui/fdr-utils": "workspace:*", "@fern-ui/loadable": "workspace:*", "@fern-ui/next-seo": "workspace:*", "@fern-ui/react-commons": "workspace:*", - "@fern-ui/search-utils": "workspace:^", + "@fern-ui/search-utils": "workspace:*", "@headlessui/react": "^1.7.18", "@inkeep/widgets": "^0.2.288", "@next/third-parties": "^14.2.4", @@ -73,7 +74,7 @@ "@shikijs/transformers": "^1.2.2", "@types/nprogress": "^0.2.3", "@vercel/edge-config": "^1.1.0", - "algoliasearch": "^4.22.1", + "algoliasearch": "^4.24.0", "bezier-easing": "^2.1.0", "clsx": "^2.1.0", "colorjs.io": "^0.5.0", @@ -90,7 +91,6 @@ "hastscript": "^9.0.0", "httpsnippet-lite": "^3.0.5", "iconoir-react": "^7.7.0", - "instantsearch.js": "^4.63.0", "jose": "^5.2.3", "jotai": "^2.8.1", "jotai-devtools": "^0.10.0", @@ -168,6 +168,7 @@ "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.16", "chromatic": "^11.3.0", + "cohere-ai": "^7.9.5", "depcheck": "^1.4.3", "eslint": "^8.56.0", "eslint-plugin-storybook": "^0.8.0", diff --git a/packages/ui/app/src/atoms/cohere.ts b/packages/ui/app/src/atoms/cohere.ts new file mode 100644 index 0000000000..994e39f08b --- /dev/null +++ b/packages/ui/app/src/atoms/cohere.ts @@ -0,0 +1,3 @@ +import { atom } from "jotai"; + +export const COHERE_ASK_AI = atom(false); diff --git a/packages/ui/app/src/atoms/index.ts b/packages/ui/app/src/atoms/index.ts index a3cf080065..e8aa7177ff 100644 --- a/packages/ui/app/src/atoms/index.ts +++ b/packages/ui/app/src/atoms/index.ts @@ -1,5 +1,6 @@ export * from "./apis"; export * from "./auth"; +export * from "./cohere"; export * from "./docs"; export * from "./files"; export * from "./flags"; diff --git a/packages/ui/app/src/css/globals.scss b/packages/ui/app/src/css/globals.scss index abf120e490..8b46a11081 100644 --- a/packages/ui/app/src/css/globals.scss +++ b/packages/ui/app/src/css/globals.scss @@ -18,3 +18,4 @@ @import "../themes"; @import "../docs/Header"; @import "../sidebar/Sidebar"; +@import "@fern-ui/chatbot/src/index"; diff --git a/packages/ui/app/src/hooks/useRouteChanged.ts b/packages/ui/app/src/hooks/useRouteChanged.ts new file mode 100644 index 0000000000..95063a9597 --- /dev/null +++ b/packages/ui/app/src/hooks/useRouteChanged.ts @@ -0,0 +1,15 @@ +import { Router } from "next/router"; +import { useEffect } from "react"; + +export function useRouteChanged(callback: () => void): void { + useEffect(() => { + Router.events.on("routeChangeComplete", callback); + Router.events.on("routeChangeError", callback); + Router.events.on("hashChangeComplete", callback); + return () => { + Router.events.off("routeChangeComplete", callback); + Router.events.off("routeChangeError", callback); + Router.events.off("hashChangeComplete", callback); + }; + }, [callback]); +} diff --git a/packages/ui/app/src/search/SearchDialog.tsx b/packages/ui/app/src/search/SearchDialog.tsx index 4289ed4d4b..375f2c182f 100644 --- a/packages/ui/app/src/search/SearchDialog.tsx +++ b/packages/ui/app/src/search/SearchDialog.tsx @@ -1,7 +1,15 @@ +import { createSearchPlaceholderWithVersion } from "@fern-ui/search-utils"; import { useAtomValue, useSetAtom } from "jotai"; +import dynamic from "next/dynamic"; import { PropsWithChildren, ReactElement, useMemo, useRef } from "react"; import { InstantSearch } from "react-instantsearch"; -import { CURRENT_VERSION_ATOM, IS_MOBILE_SCREEN_ATOM, SEARCH_DIALOG_OPEN_ATOM, useSidebarNodes } from "../atoms"; +import { + CURRENT_VERSION_ATOM, + IS_MOBILE_SCREEN_ATOM, + SEARCH_DIALOG_OPEN_ATOM, + useDomain, + useSidebarNodes, +} from "../atoms"; import { useSearchConfig } from "../services/useSearchService"; import { SidebarSearchBar } from "../sidebar/SidebarSearchBar"; import { SearchMobileHits } from "./SearchHits"; @@ -11,10 +19,15 @@ import { useAlgoliaSearchClient } from "./algolia/useAlgoliaSearchClient"; import { InkeepChatButton } from "./inkeep/InkeepChatButton"; import { InkeepCustomTrigger } from "./inkeep/InkeepCustomTrigger"; import { useSearchTrigger } from "./useSearchTrigger"; -import { createSearchPlaceholderWithVersion } from "./util"; + +const CohereChatButton = dynamic( + () => import("./cohere/CohereChatButton").then(({ CohereChatButton }) => CohereChatButton), + { ssr: false }, +); export const SearchDialog = (): ReactElement | null => { const setSearchDialogState = useSetAtom(SEARCH_DIALOG_OPEN_ATOM); + const domain = useDomain(); useSearchTrigger(setSearchDialogState); const [config] = useSearchConfig(); @@ -24,7 +37,12 @@ export const SearchDialog = (): ReactElement | null => { } if (config.inkeep == null) { - return ; + return ( + <> + + {domain.includes("cohere") && } + + ); } else { return ( <> @@ -43,7 +61,7 @@ export const SearchSidebar: React.FC> = ( const sidebar = useSidebarNodes(); const activeVersion = useAtomValue(CURRENT_VERSION_ATOM); const placeholder = useMemo( - () => createSearchPlaceholderWithVersion(activeVersion, sidebar), + () => createSearchPlaceholderWithVersion(activeVersion?.id, sidebar), [activeVersion, sidebar], ); diff --git a/packages/ui/app/src/search/SearchHit.tsx b/packages/ui/app/src/search/SearchHit.tsx index 95bdeb877a..79ea8f9cd4 100644 --- a/packages/ui/app/src/search/SearchHit.tsx +++ b/packages/ui/app/src/search/SearchHit.tsx @@ -1,4 +1,5 @@ import { visitDiscriminatedUnion } from "@fern-ui/core-utils"; +import { getSlugForSearchRecord, type SearchRecord } from "@fern-ui/search-utils"; import cn from "clsx"; import Link from "next/link"; import { ReactElement, useMemo } from "react"; @@ -9,8 +10,6 @@ import { EndpointRecordV3 } from "./content/EndpointRecordV3"; import { PageRecord } from "./content/PageRecord"; import { PageRecordV2 } from "./content/PageRecordV2"; import { PageRecordV3 } from "./content/PageRecordV3"; -import type { SearchRecord } from "./types"; -import { getSlugForSearchRecord } from "./util"; export declare namespace SearchHit { export interface Props { diff --git a/packages/ui/app/src/search/SearchHits.tsx b/packages/ui/app/src/search/SearchHits.tsx index fc5c7ecaa2..913aee4622 100644 --- a/packages/ui/app/src/search/SearchHits.tsx +++ b/packages/ui/app/src/search/SearchHits.tsx @@ -1,12 +1,11 @@ import { FernScrollArea } from "@fern-ui/components"; import { useKeyboardPress } from "@fern-ui/react-commons"; +import { getSlugForSearchRecord, type SearchRecord } from "@fern-ui/search-utils"; import { useRouter } from "next/router"; import React, { PropsWithChildren, useEffect, useMemo, useRef, useState } from "react"; import { useInfiniteHits, useInstantSearch } from "react-instantsearch"; import { useBasePath, useCloseSearchDialog } from "../atoms"; import { SearchHit } from "./SearchHit"; -import type { SearchRecord } from "./types"; -import { getSlugForSearchRecord } from "./util"; export const EmptyStateView: React.FC = ({ children }) => { return
{children}
; diff --git a/packages/ui/app/src/search/algolia/AlgoliaSearchDialog.tsx b/packages/ui/app/src/search/algolia/AlgoliaSearchDialog.tsx index 2df5ce7094..212a0cdb5c 100644 --- a/packages/ui/app/src/search/algolia/AlgoliaSearchDialog.tsx +++ b/packages/ui/app/src/search/algolia/AlgoliaSearchDialog.tsx @@ -1,15 +1,19 @@ -// import { Dialog, Transition } from "@headlessui/react"; +import { createSearchPlaceholderWithVersion } from "@fern-ui/search-utils"; import * as Dialog from "@radix-ui/react-dialog"; import { SearchClient } from "algoliasearch"; import clsx from "clsx"; import { useAtomValue, useSetAtom } from "jotai"; import { ReactElement, useMemo, useRef } from "react"; import { InstantSearch } from "react-instantsearch"; -import { CURRENT_VERSION_ATOM, POSITION_SEARCH_DIALOG_OVER_HEADER_ATOM, useSidebarNodes } from "../../atoms"; - -import { IS_MOBILE_SCREEN_ATOM, SEARCH_DIALOG_OPEN_ATOM, useIsSearchDialogOpen } from "../../atoms"; +import { + CURRENT_VERSION_ATOM, + IS_MOBILE_SCREEN_ATOM, + POSITION_SEARCH_DIALOG_OVER_HEADER_ATOM, + SEARCH_DIALOG_OPEN_ATOM, + useIsSearchDialogOpen, + useSidebarNodes, +} from "../../atoms"; import { SearchHits } from "../SearchHits"; -import { createSearchPlaceholderWithVersion } from "../util"; import { SearchBox } from "./SearchBox"; import { useAlgoliaSearchClient } from "./useAlgoliaSearchClient"; @@ -52,7 +56,7 @@ function FernInstantSearch({ searchClient, indexName, inputRef }: FernInstantSea const sidebar = useSidebarNodes(); const activeVersion = useAtomValue(CURRENT_VERSION_ATOM); const placeholder = useMemo( - () => createSearchPlaceholderWithVersion(activeVersion, sidebar), + () => createSearchPlaceholderWithVersion(activeVersion?.id, sidebar), [activeVersion, sidebar], ); return ( diff --git a/packages/ui/app/src/search/cohere/CohereChatButton.tsx b/packages/ui/app/src/search/cohere/CohereChatButton.tsx new file mode 100644 index 0000000000..ff66ea6a0c --- /dev/null +++ b/packages/ui/app/src/search/cohere/CohereChatButton.tsx @@ -0,0 +1,126 @@ +import { ChatbotMessage, ChatbotModal, Citation, CohereIcon } from "@fern-ui/chatbot"; +import * as Dialog from "@radix-ui/react-dialog"; +import type { Cohere } from "cohere-ai"; +import { useAtom } from "jotai"; +import { ReactElement, isValidElement } from "react"; +import { createPortal } from "react-dom"; +import urlJoin from "url-join"; +import { useCallbackOne } from "use-memo-one"; +import { Stream } from "../../api-playground/Stream"; +import { COHERE_ASK_AI, useBasePath } from "../../atoms"; +import { FernLink } from "../../components/FernLink"; +import { useRouteChanged } from "../../hooks/useRouteChanged"; +import { CodeBlock } from "../../mdx/components/code"; +import { useSearchConfig } from "../../services/useSearchService"; +import { BuiltWithFern } from "../../sidebar/BuiltWithFern"; + +export function CohereChatButton(): ReactElement | null { + const [config] = useSearchConfig(); + const [enabled, setEnabled] = useAtom(COHERE_ASK_AI); + const basePath = useBasePath(); + + // Close the dialog when the route changes + useRouteChanged( + useCallbackOne(() => { + setEnabled(false); + }, [setEnabled]), + ); + + const chatStream = async (message: string, conversationId: string) => { + const abortController = new AbortController(); + const body = await fetch(urlJoin(basePath || "/", "/api/fern-docs/search/cohere"), { + method: "POST", + signal: abortController.signal, + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ conversationId, message }), + }).then((res) => res.body); + + if (body == null) { + return [undefined, abortController] as const; + } + + let text = ""; + const citations: Citation[] = []; + + const stream = new Stream({ + stream: body, + parse: async (val) => { + const event = val as Cohere.StreamedChatResponse; + if (event.eventType === "text-generation") { + text += event.text; + } + + if (event.eventType === "citation-generation") { + event.citations.forEach((citation) => { + citations.push({ + text: citation.text, + start: citation.start, + end: citation.end, + slugs: citation.documentIds, + }); + }); + } + + return { message: text, role: "AI", citations }; + }, + terminator: "\n", + }); + + return [stream, abortController] as const; + }; + + if (!config.isAvailable || config.inkeep != null || typeof window === "undefined") { + return null; + } + + return ( + + {createPortal( + + + , + document.body, + )} + + + + ; + } + } + return
;
+                            },
+                            a({ href, ...props }) {
+                                if (href == null) {
+                                    return ;
+                                }
+                                return ;
+                            },
+                        }}
+                        belowInput={
+                            
+ + Powered by Cohere (command-r-plus) + + +
+ } + /> + + + + ); +} diff --git a/packages/ui/app/src/search/content/EndpointRecord.tsx b/packages/ui/app/src/search/content/EndpointRecord.tsx index 7ae29fbc90..151b95e00c 100644 --- a/packages/ui/app/src/search/content/EndpointRecord.tsx +++ b/packages/ui/app/src/search/content/EndpointRecord.tsx @@ -1,7 +1,7 @@ +import type { SearchRecord } from "@fern-ui/search-utils"; import { CodeIcon } from "@radix-ui/react-icons"; import cn from "clsx"; import { Snippet } from "react-instantsearch"; -import type { SearchRecord } from "../types"; export declare namespace EndpointRecord { export interface Props { diff --git a/packages/ui/app/src/search/content/EndpointRecordV2.tsx b/packages/ui/app/src/search/content/EndpointRecordV2.tsx index 5859a626c4..78115d7e18 100644 --- a/packages/ui/app/src/search/content/EndpointRecordV2.tsx +++ b/packages/ui/app/src/search/content/EndpointRecordV2.tsx @@ -1,7 +1,7 @@ import { visitDiscriminatedUnion } from "@fern-ui/core-utils"; +import type { EndpointSearchRecordV2 } from "@fern-ui/search-utils"; import cn from "clsx"; import { CornerDownLeft } from "react-feather"; -import type { EndpointSearchRecordV2 } from "../types"; import { SearchHitBreadCrumbs } from "./SearchHitBreadCrumbs"; export declare namespace EndpointRecordV2 { diff --git a/packages/ui/app/src/search/content/PageRecord.tsx b/packages/ui/app/src/search/content/PageRecord.tsx index dbba614d6a..07ea43b4a7 100644 --- a/packages/ui/app/src/search/content/PageRecord.tsx +++ b/packages/ui/app/src/search/content/PageRecord.tsx @@ -1,7 +1,7 @@ +import type { SearchRecord } from "@fern-ui/search-utils"; import { ActivityLogIcon } from "@radix-ui/react-icons"; import cn from "clsx"; import { Snippet } from "react-instantsearch"; -import type { SearchRecord } from "../types"; export declare namespace PageRecord { export interface Props { diff --git a/packages/ui/app/src/search/content/PageRecordV2.tsx b/packages/ui/app/src/search/content/PageRecordV2.tsx index 76bbe41ebe..1108db643f 100644 --- a/packages/ui/app/src/search/content/PageRecordV2.tsx +++ b/packages/ui/app/src/search/content/PageRecordV2.tsx @@ -1,6 +1,6 @@ +import type { PageSearchRecordV2 } from "@fern-ui/search-utils"; import cn from "clsx"; import { CornerDownLeft } from "react-feather"; -import type { PageSearchRecordV2 } from "../types"; import { SearchHitBreadCrumbs } from "./SearchHitBreadCrumbs"; export declare namespace PageRecordV2 { diff --git a/packages/ui/app/tailwind.config.cjs b/packages/ui/app/tailwind.config.cjs index 39e43e3d7a..25d1eeb70c 100644 --- a/packages/ui/app/tailwind.config.cjs +++ b/packages/ui/app/tailwind.config.cjs @@ -7,6 +7,7 @@ module.exports = { content: [ "./src/**/*.{ts,tsx}", "../tailwind.config.cjs", - path.join(path.dirname(require.resolve("@fern-ui/components")), "**/*.{ts,tsx}") + path.join(path.dirname(require.resolve("@fern-ui/components")), "**/*.{ts,tsx}"), + path.join(path.dirname(require.resolve("@fern-ui/chatbot")), "**/*.{ts,tsx}") ] }; diff --git a/packages/ui/chatbot/.eslintrc.cjs b/packages/ui/chatbot/.eslintrc.cjs new file mode 100644 index 0000000000..2d4b9f6ba8 --- /dev/null +++ b/packages/ui/chatbot/.eslintrc.cjs @@ -0,0 +1,11 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"], + ignorePatterns: ["dist", ".eslintrc.cjs"], + parser: "@typescript-eslint/parser", + plugins: ["react-refresh"], + rules: { + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }] + } +}; diff --git a/packages/ui/chatbot/.gitignore b/packages/ui/chatbot/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/packages/ui/chatbot/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/ui/chatbot/.prettierrc.cjs b/packages/ui/chatbot/.prettierrc.cjs new file mode 100644 index 0000000000..39cf0d0b8c --- /dev/null +++ b/packages/ui/chatbot/.prettierrc.cjs @@ -0,0 +1 @@ +module.exports = require("../../../.prettierrc.json"); diff --git a/packages/ui/chatbot/.stylelintrc.json b/packages/ui/chatbot/.stylelintrc.json new file mode 100644 index 0000000000..0d2e3ff61d --- /dev/null +++ b/packages/ui/chatbot/.stylelintrc.json @@ -0,0 +1 @@ +{ "extends": ["../../../shared/stylelintrc.shared.json"] } diff --git a/packages/ui/chatbot/README.md b/packages/ui/chatbot/README.md new file mode 100644 index 0000000000..85a6989e12 --- /dev/null +++ b/packages/ui/chatbot/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + project: ["./tsconfig.json", "./tsconfig.node.json", "./tsconfig.app.json"], + tsconfigRootDir: __dirname, + }, +}; +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/packages/ui/chatbot/index.html b/packages/ui/chatbot/index.html new file mode 100644 index 0000000000..e0ef3be833 --- /dev/null +++ b/packages/ui/chatbot/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/packages/ui/chatbot/package.json b/packages/ui/chatbot/package.json new file mode 100644 index 0000000000..19631bcb21 --- /dev/null +++ b/packages/ui/chatbot/package.json @@ -0,0 +1,56 @@ +{ + "name": "@fern-ui/chatbot", + "private": true, + "version": "0.0.0", + "repository": { + "type": "git", + "url": "git+https://github.com/fern-api/fern-platform.git", + "directory": "packages/ui/chatbot" + }, + "files": [ + "dist" + ], + "type": "module", + "source": "src/index.ts", + "module": "src/index.ts", + "main": "src/index.ts", + "sideEffects": [ + "*.css", + "*.scss" + ], + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@fern-ui/components": "workspace:*", + "@fern-ui/react-commons": "workspace:*", + "@radix-ui/colors": "^3.0.0", + "clsx": "^2.1.1", + "hastscript": "^9.0.0", + "jotai": "^2.8.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^9.0.1", + "remark-gfm": "^4.0.0" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.10", + "@types/hast": "^3.0.4", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.15.0", + "@typescript-eslint/parser": "^7.15.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "autoprefixer": "^10.4.16", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "postcss": "^8.4.33", + "tailwind": "^4.0.0", + "typescript": "5.4.3", + "vite": "^5.3.4" + } +} diff --git a/packages/ui/chatbot/postcss.config.js b/packages/ui/chatbot/postcss.config.js new file mode 100644 index 0000000000..49c0612d5c --- /dev/null +++ b/packages/ui/chatbot/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/packages/ui/chatbot/src/App.scss b/packages/ui/chatbot/src/App.scss new file mode 100644 index 0000000000..3fb4f14b98 --- /dev/null +++ b/packages/ui/chatbot/src/App.scss @@ -0,0 +1,59 @@ +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; +@import "@fern-ui/components/src/FernScrollArea"; +@import "@radix-ui/colors/gray"; +@import "@radix-ui/colors/gray-dark"; +@import "@radix-ui/colors/gray-alpha"; +@import "@radix-ui/colors/gray-dark-alpha"; +@import "./index"; + +@layer components { + .icon-2xl { + @apply size-8 shrink-0; + stroke-width: 1.5; + } +} + +@layer utilities { + .mask-grad-y-6 { + mask-image: linear-gradient(to bottom, transparent 0, black 24px, black calc(100% - 24px), transparent 100%); + } +} + +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; +} + +:root { + color-scheme: light dark; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + --fern-chatbot-modal-bg: #f9fafb; + --fern-chatbot-link: #3182ce; + --fern-chatbot-link-hover: #2c5282; + --tag-default: #e2e8f0; + --border-accent-muted: #e2e8f0; +} + +.dark { + --fern-chatbot-modal-bg: #1a202c; + --fern-chatbot-link: #63b3ed; + --fern-chatbot-link-hover: #4299e1; + --tag-default: #2d3748; + --border-accent-muted: #2d3748; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; + background-color: black; +} diff --git a/packages/ui/chatbot/src/App.tsx b/packages/ui/chatbot/src/App.tsx new file mode 100644 index 0000000000..908586ecbf --- /dev/null +++ b/packages/ui/chatbot/src/App.tsx @@ -0,0 +1,15 @@ +import "./App.scss"; +import { ChatbotModal } from "./components/ChatbotModal"; + +function App() { + return ( +
+ [undefined, new AbortController()]} + className="bg-grayscale-2 rounded-lg w-full text-black dark:text-white" + /> +
+ ); +} + +export default App; diff --git a/packages/ui/chatbot/src/atoms/atomWithSessionStorage.ts b/packages/ui/chatbot/src/atoms/atomWithSessionStorage.ts new file mode 100644 index 0000000000..85c807ced7 --- /dev/null +++ b/packages/ui/chatbot/src/atoms/atomWithSessionStorage.ts @@ -0,0 +1,68 @@ +import { atomWithStorage, RESET } from "jotai/utils"; +import type { WritableAtom } from "jotai/vanilla"; + +export function atomWithSessionStorage( + key: string, + initialValue: Value, +): WritableAtom Value | typeof RESET)], void> { + return atomWithStorage(key, initialValue, { + getItem: (key, initialValue) => { + if (typeof window === "undefined") { + return initialValue; + } + + try { + const stored: string | null = window.sessionStorage.getItem(key); + if (stored == null) { + return initialValue; + } + return JSON.parse(stored); + } catch (e) { + console.error(e); + } + return initialValue; + }, + setItem: (key, newValue) => { + if (typeof window === "undefined") { + return; + } + + try { + window.sessionStorage.setItem(key, JSON.stringify(newValue)); + } catch (e) { + console.error(e); + } + }, + removeItem: (key) => { + if (typeof window === "undefined") { + return; + } + + try { + window.sessionStorage.removeItem(key); + } catch (e) { + console.error(e); + } + }, + subscribe: (key, callback) => { + if (typeof window === "undefined") { + return () => {}; + } + + const listener = (event: StorageEvent) => { + if (event.key === key && event.storageArea === window.sessionStorage) { + if (event.newValue === null) { + callback(initialValue); + } else { + return initialValue; + } + } + }; + + window.addEventListener("storage", listener); + return () => { + window.removeEventListener("storage", listener); + }; + }, + }); +} diff --git a/packages/ui/chatbot/src/components/AskInput.tsx b/packages/ui/chatbot/src/components/AskInput.tsx new file mode 100644 index 0000000000..3a46cf7464 --- /dev/null +++ b/packages/ui/chatbot/src/components/AskInput.tsx @@ -0,0 +1,40 @@ +import clsx from "clsx"; +import { ReactElement, useState } from "react"; +import { SendButton } from "./SendButton"; +import { TextArea } from "./TextArea"; + +interface AskInputProps { + className?: string; + onSend: (message: string) => void; +} + +export function AskInput({ onSend, className }: AskInputProps): ReactElement { + const [value, setValue] = useState(""); + + return ( +
+
+
+
+