Bläddra i källkod

quivr addon frontend (#4648)

Feroze Mohideen 2 år sedan
förälder
incheckning
d19507822e

BIN
dashboard/src/assets/quivr.png


+ 60 - 0
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 = {

+ 20 - 0
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<typeof quivrConfigValidator>;

+ 44 - 0
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<AddonTemplate<ClientAddonType>> =
   [
     ADDON_TEMPLATE_DATADOG,
@@ -286,4 +329,5 @@ export const SUPPORTED_ADDON_TEMPLATES: Array<AddonTemplate<ClientAddonType>> =
     ADDON_TEMPLATE_METABASE,
     // ADDON_TEMPLATE_NEWRELIC,
     ADDON_TEMPLATE_TAILSCALE,
+    ADDON_TEMPLATE_QUIVR,
   ];

+ 3 - 0
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]);
 

+ 2 - 0
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<Props> = ({ type }) => {
     .with("metabase", () => <MetabaseForm />)
     .with("newrelic", () => <NewRelicForm />)
     .with("tailscale", () => <TailscaleForm />)
+    .with("quivr", () => <QuivrForm />)
     .otherwise(() => null);
 };
 

+ 208 - 0
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<ClientAddon>();
+  const watchExposedToExternalTraffic = watch(
+    "config.exposedToExternalTraffic",
+    false
+  );
+
+  return (
+    <div>
+      <Text size={16}>Quivr configuration</Text>
+      <Spacer y={0.5} />
+      <Controller
+        name={"config.exposedToExternalTraffic"}
+        control={control}
+        render={({ field: { value, onChange } }) => (
+          <Checkbox
+            checked={value}
+            toggleChecked={() => {
+              onChange(!value);
+            }}
+          >
+            <Text>Expose to external traffic</Text>
+          </Checkbox>
+        )}
+      />
+      <CollapsibleContainer isOpened={watchExposedToExternalTraffic}>
+        <Spacer y={0.5} />
+        <Text>Custom domain</Text>
+        <Spacer y={0.5} />
+        <Text color="helper">
+          Add an optional custom domain to access Quivr. If you do not provide a
+          custom domain, Porter will provision a domain for you.
+        </Text>
+        {cluster.ingress_ip !== "" && (
+          <>
+            <Spacer y={0.5} />
+            <div style={{ width: "100%" }}>
+              <Text color="helper">
+                To configure a custom domain, you must add{" "}
+                {stringifiedDNSRecordType(cluster.ingress_ip)} pointing to the
+                following Ingress IP for your cluster:{" "}
+              </Text>
+            </div>
+            <Spacer y={0.5} />
+            <IdContainer>
+              <Code>{cluster.ingress_ip}</Code>
+              <CopyContainer>
+                <CopyToClipboard text={cluster.ingress_ip}>
+                  <CopyIcon src={copy} alt="copy" />
+                </CopyToClipboard>
+              </CopyContainer>
+            </IdContainer>
+            <Spacer y={0.5} />
+          </>
+        )}
+        <ControlledInput
+          type="text"
+          width="300px"
+          {...register("config.customDomain")}
+          placeholder="api.quivr.my-domain.com"
+          error={errors.config?.customDomain?.message}
+        />
+      </CollapsibleContainer>
+      <Spacer y={1} />
+      <Text>Quivr Domain</Text>
+      <Spacer y={0.5} />
+      <ControlledInput
+        type="text"
+        width="300px"
+        {...register("config.quivrDomain")}
+        placeholder="https://chat.quivr.com"
+        error={errors.config?.quivrDomain?.message}
+      />
+      <Spacer y={1} />
+      <Text>OpenAI API Key</Text>
+      <Spacer y={0.5} />
+      <ControlledInput
+        type="password"
+        width="300px"
+        {...register("config.openAiApiKey")}
+        placeholder="*****"
+        error={errors.config?.openAiApiKey?.message}
+      />
+      <Spacer y={1} />
+      <Text>Supabase URL</Text>
+      <Spacer y={0.5} />
+      <ControlledInput
+        type="password"
+        width="300px"
+        {...register("config.supabaseUrl")}
+        placeholder="https://*******.supabase.co"
+        error={errors.config?.supabaseUrl?.message}
+      />
+      <Spacer y={1} />
+      <Text>Supabase Service Key</Text>
+      <Spacer y={0.5} />
+      <ControlledInput
+        type="password"
+        width="300px"
+        {...register("config.supabaseServiceKey")}
+        placeholder="*****"
+        error={errors.config?.supabaseServiceKey?.message}
+      />
+      <Spacer y={1} />
+      <Text>PostgreSQL Database URL</Text>
+      <Spacer y={0.5} />
+      <ControlledInput
+        type="password"
+        width="300px"
+        {...register("config.pgDatabaseUrl")}
+        placeholder="postgres://postgres:postgres@my-pg-host.com:5432/quivr"
+        error={errors.config?.pgDatabaseUrl?.message}
+      />
+      <Spacer y={1} />
+      <Text>JWT Secret Token</Text>
+      <Spacer y={0.5} />
+      <ControlledInput
+        type="password"
+        width="300px"
+        {...register("config.jwtSecretKey")}
+        placeholder="uper-secret-jwt-token-with-at-least-32-characters-long"
+        error={errors.config?.jwtSecretKey?.message}
+      />
+      <Spacer y={1} />
+      <Text>Anthropic API Key</Text>
+      <Spacer y={0.5} />
+      <ControlledInput
+        type="password"
+        width="300px"
+        {...register("config.anthropicApiKey")}
+        placeholder="*****"
+        error={errors.config?.anthropicApiKey?.message}
+      />
+      <Spacer y={1} />
+      <Text>Cohere API Key</Text>
+      <Spacer y={0.5} />
+      <ControlledInput
+        type="password"
+        width="300px"
+        {...register("config.cohereApiKey")}
+        placeholder="*****"
+        error={errors.config?.cohereApiKey?.message}
+      />
+      <Spacer y={1} />
+      <AddonSaveButton />
+    </div>
+  );
+};
+
+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;
+  }
+`;