diff --git a/dashboard/src/assets/quivr.png b/dashboard/src/assets/quivr.png new file mode 100644 index 0000000000..2df464f0ad Binary files /dev/null and b/dashboard/src/assets/quivr.png differ diff --git a/dashboard/src/lib/addons/index.ts b/dashboard/src/lib/addons/index.ts index d836fe3025..89e04281fd 100644 --- a/dashboard/src/lib/addons/index.ts +++ b/dashboard/src/lib/addons/index.ts @@ -7,6 +7,7 @@ import { Mezmo, Newrelic, Postgres, + Quivr, Redis, Tailscale, } from "@porter-dev/api-contracts/src/porter/v1/addons_pb"; @@ -20,6 +21,7 @@ import { metabaseConfigValidator } from "./metabase"; import { mezmoConfigValidator } from "./mezmo"; import { newrelicConfigValidator } from "./newrelic"; import { defaultPostgresAddon, postgresConfigValidator } from "./postgres"; +import { quivrConfigValidator } from "./quivr"; import { redisConfigValidator } from "./redis"; import { tailscaleConfigValidator } from "./tailscale"; import { @@ -28,6 +30,7 @@ import { ADDON_TEMPLATE_MEZMO, ADDON_TEMPLATE_NEWRELIC, ADDON_TEMPLATE_POSTGRES, + ADDON_TEMPLATE_QUIVR, ADDON_TEMPLATE_REDIS, ADDON_TEMPLATE_TAILSCALE, type AddonTemplate, @@ -55,6 +58,7 @@ export const clientAddonValidator = z.object({ metabaseConfigValidator, newrelicConfigValidator, tailscaleConfigValidator, + quivrConfigValidator, ]), }); export type ClientAddonType = z.infer< @@ -153,6 +157,16 @@ export function defaultClientAddon( }), template: ADDON_TEMPLATE_TAILSCALE, })) + .with("quivr", () => ({ + ...clientAddonValidator.parse({ + expanded: true, + name: { readOnly: false, value: "quivr" }, + config: quivrConfigValidator.parse({ + type: "quivr", + }), + }), + template: ADDON_TEMPLATE_QUIVR, + })) .exhaustive(); } @@ -165,6 +179,7 @@ function addonTypeEnumProto(type: ClientAddon["config"]["type"]): AddonType { .with("metabase", () => AddonType.METABASE) .with("newrelic", () => AddonType.NEWRELIC) .with("tailscale", () => AddonType.TAILSCALE) + .with("quivr", () => AddonType.QUIVR) .exhaustive(); } @@ -254,6 +269,31 @@ export function clientAddonToProto( }), case: "tailscale" as const, })) + .with({ type: "quivr" }, (data) => ({ + value: new Quivr({ + ingressEnabled: data.exposedToExternalTraffic, + domains: [ + { + name: data.customDomain, + type: DomainType.UNSPECIFIED, + }, + { + name: data.porterDomain, + type: DomainType.PORTER, + }, + // if not exposed, remove all domains + ].filter((d) => d.name !== "" && data.exposedToExternalTraffic), + openaiApiKey: data.openAiApiKey, + supabaseUrl: data.supabaseUrl, + supabaseServiceKey: data.supabaseServiceKey, + pgDatabaseUrl: data.pgDatabaseUrl, + jwtSecretKey: data.jwtSecretKey, + quivrDomain: data.quivrDomain, + anthropicApiKey: data.anthropicApiKey, + cohereApiKey: data.cohereApiKey, + }), + case: "quivr" as const, + })) .exhaustive(); const proto = new Addon({ @@ -365,6 +405,25 @@ export function clientAddonFromProto({ authKey: data.value.authKey ?? "", subnetRoutes: data.value.subnetRoutes.map((r) => ({ route: r })), })) + .with({ case: "quivr" }, (data) => ({ + type: "quivr" as const, + exposedToExternalTraffic: data.value.ingressEnabled ?? false, + porterDomain: + data.value.domains.find((domain) => domain.type === DomainType.PORTER) + ?.name ?? "", + customDomain: + data.value.domains.find( + (domain) => domain.type === DomainType.UNSPECIFIED + )?.name ?? "", + openAiApiKey: data.value.openaiApiKey ?? "", + supabaseUrl: data.value.supabaseUrl ?? "", + supabaseServiceKey: data.value.supabaseServiceKey ?? "", + pgDatabaseUrl: data.value.pgDatabaseUrl ?? "", + jwtSecretKey: data.value.jwtSecretKey ?? "", + quivrDomain: data.value.quivrDomain ?? "", + anthropicApiKey: data.value.anthropicApiKey ?? "", + cohereApiKey: data.value.cohereApiKey ?? "", + })) .exhaustive(); const template = match(addon.config) @@ -375,6 +434,7 @@ export function clientAddonFromProto({ .with({ case: "metabase" }, () => ADDON_TEMPLATE_METABASE) .with({ case: "newrelic" }, () => ADDON_TEMPLATE_NEWRELIC) .with({ case: "tailscale" }, () => ADDON_TEMPLATE_TAILSCALE) + .with({ case: "quivr" }, () => ADDON_TEMPLATE_QUIVR) .exhaustive(); const clientAddon = { diff --git a/dashboard/src/lib/addons/quivr.ts b/dashboard/src/lib/addons/quivr.ts new file mode 100644 index 0000000000..b5209d1e7d --- /dev/null +++ b/dashboard/src/lib/addons/quivr.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +export const quivrConfigValidator = z.object({ + type: z.literal("quivr"), + exposedToExternalTraffic: z.boolean().default(true), + porterDomain: z.string().default(""), + customDomain: z.string().default(""), + openAiApiKey: z.string().nonempty().default("*******"), + supabaseUrl: z.string().nonempty().default("https://*******.supabase.co"), + supabaseServiceKey: z.string().nonempty().default("*******"), + pgDatabaseUrl: z + .string() + .nonempty() + .default("postgres://postgres:postgres@localhost:5432/quivr"), + jwtSecretKey: z.string().nonempty().default("*******"), + quivrDomain: z.string().nonempty().default("https://*******.quivr.co"), + anthropicApiKey: z.string().nonempty().default("*******"), + cohereApiKey: z.string().nonempty().default("*******"), +}); +export type QuivrConfigValidator = z.infer; diff --git a/dashboard/src/lib/addons/template.ts b/dashboard/src/lib/addons/template.ts index e77083e335..ba409ced3c 100644 --- a/dashboard/src/lib/addons/template.ts +++ b/dashboard/src/lib/addons/template.ts @@ -4,9 +4,12 @@ import DatadogForm from "main/home/add-on-dashboard/datadog/DatadogForm"; import MetabaseForm from "main/home/add-on-dashboard/metabase/MetabaseForm"; import MezmoForm from "main/home/add-on-dashboard/mezmo/MezmoForm"; import NewRelicForm from "main/home/add-on-dashboard/newrelic/NewRelicForm"; +import QuivrForm from "main/home/add-on-dashboard/quivr/QuivrForm"; import TailscaleForm from "main/home/add-on-dashboard/tailscale/TailscaleForm"; import TailscaleOverview from "main/home/add-on-dashboard/tailscale/TailscaleOverview"; +import quivr from "assets/quivr.png"; + import { type ClientAddon, type ClientAddonType } from "."; export type AddonTemplateTag = @@ -279,6 +282,46 @@ export const ADDON_TEMPLATE_TAILSCALE: AddonTemplate<"tailscale"> = { }, }; +export const ADDON_TEMPLATE_QUIVR: AddonTemplate<"quivr"> = { + type: "quivr", + displayName: "Quivr", + description: "Your second brain, empowered by generative AI", + icon: quivr, + tags: ["Analytics"], + tabs: [ + { + name: "configuration", + displayName: "Configuration", + component: QuivrForm, + }, + { + name: "logs", + displayName: "Logs", + component: Logs, + isOnlyForPorterOperators: true, + }, + { + name: "settings", + displayName: "Settings", + component: Settings, + }, + ], + defaultValues: { + type: "quivr", + exposedToExternalTraffic: true, + porterDomain: "", + customDomain: "", + openAiApiKey: "", + supabaseUrl: "", + supabaseServiceKey: "", + pgDatabaseUrl: "", + jwtSecretKey: "", + quivrDomain: "https://chat.quivr.com", + anthropicApiKey: "", + cohereApiKey: "", + }, +}; + export const SUPPORTED_ADDON_TEMPLATES: Array> = [ ADDON_TEMPLATE_DATADOG, @@ -286,4 +329,5 @@ export const SUPPORTED_ADDON_TEMPLATES: Array> = ADDON_TEMPLATE_METABASE, // ADDON_TEMPLATE_NEWRELIC, ADDON_TEMPLATE_TAILSCALE, + ADDON_TEMPLATE_QUIVR, ]; diff --git a/dashboard/src/main/home/add-on-dashboard/AddonHeader.tsx b/dashboard/src/main/home/add-on-dashboard/AddonHeader.tsx index e908db2a93..1ec6e70d7d 100644 --- a/dashboard/src/main/home/add-on-dashboard/AddonHeader.tsx +++ b/dashboard/src/main/home/add-on-dashboard/AddonHeader.tsx @@ -20,6 +20,9 @@ const AddonHeader: React.FC = () => { .with({ type: "metabase" }, (config) => { return config.customDomain || config.porterDomain; }) + .with({ type: "quivr" }, (config) => { + return config.customDomain || config.porterDomain; + }) .otherwise(() => ""); }, [addon]); diff --git a/dashboard/src/main/home/add-on-dashboard/common/Configuration.tsx b/dashboard/src/main/home/add-on-dashboard/common/Configuration.tsx index 093f1babd0..92f0afa50c 100644 --- a/dashboard/src/main/home/add-on-dashboard/common/Configuration.tsx +++ b/dashboard/src/main/home/add-on-dashboard/common/Configuration.tsx @@ -7,6 +7,7 @@ import DatadogForm from "../datadog/DatadogForm"; import MetabaseForm from "../metabase/MetabaseForm"; import MezmoForm from "../mezmo/MezmoForm"; import NewRelicForm from "../newrelic/NewRelicForm"; +import QuivrForm from "../quivr/QuivrForm"; import TailscaleForm from "../tailscale/TailscaleForm"; type Props = { @@ -20,6 +21,7 @@ const Configuration: React.FC = ({ type }) => { .with("metabase", () => ) .with("newrelic", () => ) .with("tailscale", () => ) + .with("quivr", () => ) .otherwise(() => null); }; diff --git a/dashboard/src/main/home/add-on-dashboard/quivr/QuivrForm.tsx b/dashboard/src/main/home/add-on-dashboard/quivr/QuivrForm.tsx new file mode 100644 index 0000000000..b126ce8562 --- /dev/null +++ b/dashboard/src/main/home/add-on-dashboard/quivr/QuivrForm.tsx @@ -0,0 +1,208 @@ +import React from "react"; +import { Controller, useFormContext } from "react-hook-form"; +import styled from "styled-components"; + +import CopyToClipboard from "components/CopyToClipboard"; +import Checkbox from "components/porter/Checkbox"; +import CollapsibleContainer from "components/porter/CollapsibleContainer"; +import { ControlledInput } from "components/porter/ControlledInput"; +import Spacer from "components/porter/Spacer"; +import Text from "components/porter/Text"; +import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider"; +import { type ClientAddon } from "lib/addons"; + +import { stringifiedDNSRecordType } from "utils/ip"; +import copy from "assets/copy-left.svg"; + +import AddonSaveButton from "../AddonSaveButton"; + +const QuivrForm: React.FC = () => { + const { cluster } = useClusterContext(); + + const { + register, + formState: { errors }, + control, + watch, + } = useFormContext(); + const watchExposedToExternalTraffic = watch( + "config.exposedToExternalTraffic", + false + ); + + return ( +
+ Quivr configuration + + ( + { + onChange(!value); + }} + > + Expose to external traffic + + )} + /> + + + Custom domain + + + Add an optional custom domain to access Quivr. If you do not provide a + custom domain, Porter will provision a domain for you. + + {cluster.ingress_ip !== "" && ( + <> + +
+ + To configure a custom domain, you must add{" "} + {stringifiedDNSRecordType(cluster.ingress_ip)} pointing to the + following Ingress IP for your cluster:{" "} + +
+ + + {cluster.ingress_ip} + + + + + + + + + )} + +
+ + Quivr Domain + + + + OpenAI API Key + + + + Supabase URL + + + + Supabase Service Key + + + + PostgreSQL Database URL + + + + JWT Secret Token + + + + Anthropic API Key + + + + Cohere API Key + + + + +
+ ); +}; + +export default QuivrForm; + +const Code = styled.span` + font-family: monospace; +`; + +const IdContainer = styled.div` + background: #26292e; + border-radius: 5px; + padding: 10px; + display: flex; + width: 100%; + border-radius: 5px; + border: 1px solid ${({ theme }) => theme.border}; + align-items: center; + user-select: text; +`; + +const CopyContainer = styled.div` + display: flex; + align-items: center; + margin-left: auto; +`; + +const CopyIcon = styled.img` + cursor: pointer; + margin-left: 5px; + margin-right: 5px; + width: 15px; + height: 15px; + :hover { + opacity: 0.8; + } +`;