Browse Source

feat: hoppscotch-common & platform additions for ai experiments (#4222)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
Akash K 7 months ago
parent
commit
d68cfb313e

+ 7 - 1
packages/hoppscotch-common/locales/en.json

@@ -612,7 +612,8 @@
     "url": "URL",
     "url_placeholder": "Enter a URL or paste a cURL command",
     "variables": "Variables",
-    "view_my_links": "View my links"
+    "view_my_links": "View my links",
+    "generate_name_error": "Failed to generate request name."
   },
   "response": {
     "audio": "Audio",
@@ -672,6 +673,7 @@
     "short_codes": "Short codes",
     "short_codes_description": "Short codes which were created by you.",
     "sidebar_on_left": "Sidebar on left",
+    "ai_experiments": "AI Experiments",
     "sync": "Synchronise",
     "sync_collections": "Collections",
     "sync_description": "These settings are synced to cloud.",
@@ -1084,5 +1086,9 @@
     "cli_command_generation_description_sh": "Copy the below command and run it from the CLI. Please specify a personal access token and verify the generated SH instance server URL.",
     "cli_command_generation_description_sh_with_server_url_placeholder": "Copy the below command and run it from the CLI. Please specify a personal access token and the SH instance server URL.",
     "run_collection": "Run collection"
+  },
+  "ai_experiments": {
+    "generate_request_name": "Generate Request Name Using AI",
+    "generate_or_modify_request_body": "Generate or Modify Request Body"
   }
 }

+ 77 - 7
packages/hoppscotch-common/src/components/collections/EditRequest.vue

@@ -6,13 +6,28 @@
     @close="hideModal"
   >
     <template #body>
-      <HoppSmartInput
-        v-model="editingName"
-        placeholder=" "
-        :label="t('action.label')"
-        input-styles="floating-input"
-        @submit="editRequest"
-      />
+      <div class="flex gap-1">
+        <HoppSmartInput
+          v-model="editingName"
+          class="flex-grow"
+          placeholder=" "
+          :label="t('action.label')"
+          input-styles="floating-input"
+          @submit="editRequest"
+        />
+        <HoppButtonSecondary
+          v-if="showGenerateRequestNameButton"
+          v-tippy="{ theme: 'tooltip' }"
+          :icon="IconSparkle"
+          :disabled="isGenerateRequestNamePending"
+          class="rounded-md"
+          :class="{
+            'animate-pulse': isGenerateRequestNamePending,
+          }"
+          :title="t('ai_experiments.generate_request_name')"
+          @click="generateRequestName"
+        />
+      </div>
     </template>
     <template #footer>
       <span class="flex space-x-2">
@@ -36,7 +51,15 @@
 <script setup lang="ts">
 import { useI18n } from "@composables/i18n"
 import { useToast } from "@composables/toast"
+import { HoppRESTRequest } from "@hoppscotch/data"
 import { useVModel } from "@vueuse/core"
+import * as E from "fp-ts/Either"
+import { computed, ref } from "vue"
+
+import { useSetting } from "~/composables/settings"
+import { useReadonlyStream } from "~/composables/stream"
+import { platform } from "~/platform"
+import IconSparkle from "~icons/lucide/sparkles"
 
 const toast = useToast()
 const t = useI18n()
@@ -46,6 +69,7 @@ const props = withDefaults(
     show: boolean
     loadingState: boolean
     modelValue?: string
+    requestContext: HoppRESTRequest | null
   }>(),
   {
     show: false,
@@ -60,8 +84,54 @@ const emit = defineEmits<{
   (e: "update:modelValue", value: string): void
 }>()
 
+const ENABLE_AI_EXPERIMENTS = useSetting("ENABLE_AI_EXPERIMENTS")
+
 const editingName = useVModel(props, "modelValue")
 
+const currentUser = useReadonlyStream(
+  platform.auth.getCurrentUserStream(),
+  platform.auth.getCurrentUser()
+)
+
+const isGenerateRequestNamePending = ref(false)
+
+const showGenerateRequestNameButton = computed(() => {
+  // Request generation applies only to the authenticated state
+  if (!currentUser.value) {
+    return false
+  }
+
+  return ENABLE_AI_EXPERIMENTS.value && !!platform.experiments?.aiExperiments
+})
+
+const generateRequestName = async () => {
+  const generateRequestNameForPlatform =
+    platform.experiments?.aiExperiments?.generateRequestName
+
+  if (!props.requestContext || !generateRequestNameForPlatform) {
+    toast.error(t("request.generate_name_error"))
+    return
+  }
+
+  isGenerateRequestNamePending.value = true
+
+  const result = await generateRequestNameForPlatform(
+    JSON.stringify(props.requestContext)
+  )
+
+  if (result && E.isLeft(result)) {
+    toast.error(t("request.generate_name_error"))
+
+    isGenerateRequestNamePending.value = false
+
+    return
+  }
+
+  editingName.value = result.right
+
+  isGenerateRequestNamePending.value = false
+}
+
 const editRequest = () => {
   if (editingName.value.trim() === "") {
     toast.error(t("request.invalid_name"))

+ 76 - 8
packages/hoppscotch-common/src/components/collections/graphql/EditRequest.vue

@@ -6,13 +6,28 @@
     @close="hideModal"
   >
     <template #body>
-      <HoppSmartInput
-        v-model="requestUpdateData.name"
-        placeholder=" "
-        :label="t('action.label')"
-        input-styles="floating-input"
-        @submit="saveRequest"
-      />
+      <div class="flex gap-1">
+        <HoppSmartInput
+          v-model="requestUpdateData.name"
+          class="flex-grow"
+          placeholder=" "
+          :label="t('action.label')"
+          input-styles="floating-input"
+          @submit="saveRequest"
+        />
+        <HoppButtonSecondary
+          v-if="showGenerateRequestNameButton"
+          v-tippy="{ theme: 'tooltip' }"
+          :icon="IconSparkle"
+          :disabled="isGenerateRequestNamePending"
+          class="rounded-md"
+          :class="{
+            'animate-pulse': isGenerateRequestNamePending,
+          }"
+          :title="t('ai_experiments.generate_request_name')"
+          @click="generateRequestName"
+        />
+      </div>
     </template>
     <template #footer>
       <span class="flex space-x-2">
@@ -33,11 +48,17 @@
 </template>
 
 <script setup lang="ts">
-import { ref, watch } from "vue"
 import { useI18n } from "@composables/i18n"
 import { useToast } from "@composables/toast"
 import { HoppGQLRequest } from "@hoppscotch/data"
+import * as E from "fp-ts/Either"
+import { computed, ref, watch } from "vue"
+
+import { useSetting } from "~/composables/settings"
+import { useReadonlyStream } from "~/composables/stream"
 import { editGraphqlRequest } from "~/newstore/collections"
+import { platform } from "~/platform"
+import IconSparkle from "~icons/lucide/sparkles"
 
 const t = useI18n()
 const toast = useToast()
@@ -48,6 +69,7 @@ const props = defineProps<{
   requestIndex: number | null
   request: HoppGQLRequest | null
   editingRequestName: string
+  requestContext: HoppGQLRequest | null
 }>()
 
 const emit = defineEmits<{
@@ -63,6 +85,52 @@ watch(
   }
 )
 
+const ENABLE_AI_EXPERIMENTS = useSetting("ENABLE_AI_EXPERIMENTS")
+
+const currentUser = useReadonlyStream(
+  platform.auth.getCurrentUserStream(),
+  platform.auth.getCurrentUser()
+)
+
+const isGenerateRequestNamePending = ref(false)
+
+const showGenerateRequestNameButton = computed(() => {
+  // Request generation applies only to the authenticated state
+  if (!currentUser.value) {
+    return false
+  }
+
+  return ENABLE_AI_EXPERIMENTS.value && !!platform.experiments?.aiExperiments
+})
+
+const generateRequestName = async () => {
+  const generateRequestNameForPlatform =
+    platform.experiments?.aiExperiments?.generateRequestName
+
+  if (!props.requestContext || !generateRequestNameForPlatform) {
+    toast.error(t("request.generate_name_error"))
+    return
+  }
+
+  isGenerateRequestNamePending.value = true
+
+  const result = await generateRequestNameForPlatform(
+    JSON.stringify(props.requestContext)
+  )
+
+  if (result && E.isLeft(result)) {
+    toast.error(t("request.generate_name_error"))
+
+    isGenerateRequestNamePending.value = false
+
+    return
+  }
+
+  requestUpdateData.value.name = result.right
+
+  isGenerateRequestNamePending.value = false
+}
+
 const saveRequest = () => {
   if (!requestUpdateData.value.name) {
     toast.error(`${t("collection.invalid_name")}`)

+ 1 - 0
packages/hoppscotch-common/src/components/collections/graphql/index.vue

@@ -139,6 +139,7 @@
       :folder-path="editingFolderPath"
       :request="editingRequest"
       :request-index="editingRequestIndex"
+      :request-context="editingRequest"
       :editing-request-name="editingRequest ? editingRequest.name : ''"
       @hide-modal="displayModalEditRequest(false)"
     />

+ 1 - 0
packages/hoppscotch-common/src/components/collections/index.vue

@@ -142,6 +142,7 @@
     <CollectionsEditRequest
       v-model="editingRequestName"
       :show="showModalEditRequest"
+      :request-context="editingRequest"
       :loading-state="modalLoadingState"
       @submit="updateEditingRequest"
       @hide-modal="displayModalEditRequest(false)"

+ 2 - 0
packages/hoppscotch-common/src/newstore/settings.ts

@@ -66,6 +66,7 @@ export type SettingsDef = {
   COLUMN_LAYOUT: boolean
 
   HAS_OPENED_SPOTLIGHT: boolean
+  ENABLE_AI_EXPERIMENTS: boolean
 }
 
 export const getDefaultSettings = (): SettingsDef => ({
@@ -113,6 +114,7 @@ export const getDefaultSettings = (): SettingsDef => ({
   COLUMN_LAYOUT: true,
 
   HAS_OPENED_SPOTLIGHT: false,
+  ENABLE_AI_EXPERIMENTS: false,
 })
 
 type ApplySettingPayload = {

+ 7 - 0
packages/hoppscotch-common/src/pages/graphql.vue

@@ -62,6 +62,7 @@
     <CollectionsEditRequest
       v-model="editReqModalReqName"
       :show="showRenamingReqNameModalForTabID !== undefined"
+      :request-context="requestToRename"
       @submit="renameReqName"
       @hide-modal="showRenamingReqNameModalForTabID = undefined"
     />
@@ -184,6 +185,12 @@ onBeforeUnmount(() => {
 const editReqModalReqName = ref("")
 const showRenamingReqNameModalForTabID = ref<string>()
 
+const requestToRename = computed(() => {
+  if (!showRenamingReqNameModalForTabID.value) return null
+  const tab = tabs.getTabRef(showRenamingReqNameModalForTabID.value)
+  return tab.value.document.request
+})
+
 const openReqRenameModal = (tab: HoppTab<HoppGQLDocument>) => {
   editReqModalReqName.value = tab.document.request.name
   showRenamingReqNameModalForTabID.value = tab.id

+ 12 - 2
packages/hoppscotch-common/src/pages/index.vue

@@ -60,6 +60,7 @@
     </AppPaneLayout>
     <CollectionsEditRequest
       v-model="reqName"
+      :request-context="requestToRename"
       :show="showRenamingReqNameModal"
       @submit="renameReqName"
       @hide-modal="showRenamingReqNameModal = false"
@@ -118,7 +119,7 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, onMounted } from "vue"
+import { ref, onMounted, computed } from "vue"
 import { safelyExtractRESTRequest } from "@hoppscotch/data"
 import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
 import { useRoute } from "vue-router"
@@ -256,13 +257,22 @@ const onResolveConfirmCloseAllTabs = () => {
   confirmingCloseAllTabs.value = false
 }
 
+const requestToRename = computed(() => {
+  if (!renameTabID.value) return null
+  const tab = tabs.getTabRef(renameTabID.value)
+  return tab.value.document.request
+})
+
 const openReqRenameModal = (tabID?: string) => {
   if (tabID) {
     const tab = tabs.getTabRef(tabID)
     reqName.value = tab.value.document.request.name
     renameTabID.value = tabID
   } else {
-    reqName.value = tabs.currentActiveTab.value.document.request.name
+    const { id, document } = tabs.currentActiveTab.value
+
+    reqName.value = document.request.name
+    renameTabID.value = id
   }
   showRenamingReqNameModal.value = true
 }

+ 12 - 0
packages/hoppscotch-common/src/pages/settings.vue

@@ -83,6 +83,14 @@
                   {{ t("settings.sidebar_on_left") }}
                 </HoppSmartToggle>
               </div>
+              <div v-if="hasAIExperimentsSupport" class="flex items-center">
+                <HoppSmartToggle
+                  :on="ENABLE_AI_EXPERIMENTS"
+                  @change="toggleSetting('ENABLE_AI_EXPERIMENTS')"
+                >
+                  {{ t("settings.ai_experiments") }}
+                </HoppSmartToggle>
+              </div>
             </div>
           </section>
         </div>
@@ -179,6 +187,7 @@ const PROXY_URL = useSetting("PROXY_URL")
 const TELEMETRY_ENABLED = useSetting("TELEMETRY_ENABLED")
 const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
 const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
+const ENABLE_AI_EXPERIMENTS = useSetting("ENABLE_AI_EXPERIMENTS")
 
 const hasPlatformTelemetry = Boolean(platform.platformFeatureFlags.hasTelemetry)
 
@@ -188,6 +197,9 @@ const proxySettings = computed(() => ({
   url: PROXY_URL.value,
 }))
 
+const hasAIExperimentsSupport =
+  !!platform.experiments?.aiExperiments?.enableAIExperiments
+
 watch(
   proxySettings,
   ({ url }) => {

+ 10 - 0
packages/hoppscotch-common/src/platform/experiments.ts

@@ -0,0 +1,10 @@
+import * as E from "fp-ts/Either"
+
+export type ExperimentsPlatformDef = {
+  aiExperiments?: {
+    enableAIExperiments: boolean
+    generateRequestName: (
+      requestInfo: string
+    ) => Promise<E.Either<string, string>>
+  }
+}

Some files were not shown because too many files changed in this diff