feat: modify body with ai & feedback on ai requests hoppscotch-common bindings (#4386)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
@@ -1138,6 +1138,14 @@
|
||||
},
|
||||
"ai_experiments": {
|
||||
"generate_request_name": "Generate Request Name Using AI",
|
||||
"generate_or_modify_request_body": "Generate or Modify Request Body"
|
||||
"generate_or_modify_request_body": "Generate or Modify Request Body",
|
||||
"modify_with_ai": "Modify with AI",
|
||||
"generate_or_modify_request_body_input_placeholder": "Enter your prompt to modify request body",
|
||||
"accept_change": "Accept Change",
|
||||
"feedback_success": "Feedback submitted successfully",
|
||||
"feedback_failure": "Failed to submit feedback",
|
||||
"feedback_thank_you": "Thank you for your feedback!",
|
||||
"feedback_cta_text_long": "Rate the generation, helps us to improve",
|
||||
"feedback_cta_request_name": "Did you like name generated?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"@codemirror/language": "6.10.1",
|
||||
"@codemirror/legacy-modes": "6.3.3",
|
||||
"@codemirror/lint": "6.5.0",
|
||||
"@codemirror/merge": "6.7.0",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.25.1",
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { jsonLanguage } from "@codemirror/lang-json"
|
||||
import { MergeView } from "@codemirror/merge"
|
||||
import { onUnmounted, ref, watch } from "vue"
|
||||
import { basicSetup, baseTheme } from "@helpers/editor/themes/baseTheme"
|
||||
import { EditorState } from "@codemirror/state"
|
||||
|
||||
type MergeViewContent = {
|
||||
content: string
|
||||
langMime: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
contentLeft: MergeViewContent
|
||||
contentRight: MergeViewContent
|
||||
}>()
|
||||
|
||||
const diffEditor = ref<Element | null>(null)
|
||||
let mergeView: MergeView | null = null
|
||||
|
||||
watch(
|
||||
() => props.contentRight,
|
||||
() => {
|
||||
if (!mergeView) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!props.contentRight.content) {
|
||||
return
|
||||
}
|
||||
|
||||
mergeView.b.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: mergeView.b.state.doc.length,
|
||||
insert: props.contentRight.content,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
diffEditor,
|
||||
() => {
|
||||
if (!diffEditor.value) {
|
||||
return
|
||||
}
|
||||
|
||||
mergeView = new MergeView({
|
||||
a: {
|
||||
doc: props.contentLeft.content,
|
||||
extensions: [
|
||||
jsonLanguage,
|
||||
basicSetup,
|
||||
baseTheme,
|
||||
EditorState.readOnly.of(true),
|
||||
],
|
||||
},
|
||||
b: {
|
||||
doc: props.contentRight.content,
|
||||
extensions: [jsonLanguage, baseTheme, basicSetup],
|
||||
},
|
||||
// @ts-expect-error attribute mismatch
|
||||
parent: diffEditor.value,
|
||||
highlightChanges: false,
|
||||
})
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (mergeView) {
|
||||
mergeView.destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="diffEditor"></div>
|
||||
</template>
|
||||
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import IconArrowRight from "~icons/lucide/arrow-right"
|
||||
import IconThumbsUp from "~icons/lucide/thumbs-up"
|
||||
import IconThumbsDown from "~icons/lucide/thumbs-down"
|
||||
import {
|
||||
useModifyRequestBody,
|
||||
useSubmitFeedback,
|
||||
} from "~/composables/ai-experiments"
|
||||
import { ref } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
currentBody: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "closeModal"): void
|
||||
(e: "updateBody", body: string): void
|
||||
}>()
|
||||
|
||||
const generatedBodyContent = ref("")
|
||||
|
||||
const userPrompt = ref("")
|
||||
|
||||
const { modifyRequestBody, isModifyRequestBodyPending, lastTraceID } =
|
||||
useModifyRequestBody(props.currentBody, userPrompt, generatedBodyContent)
|
||||
|
||||
const submittedFeedback = ref(false)
|
||||
|
||||
const { submitFeedback, isSubmitFeedbackPending } = useSubmitFeedback()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HoppSmartModal styles="sm:max-w-3xl" full-width>
|
||||
<template #body>
|
||||
<div class="flex flex-col border-b border-divider transition relative">
|
||||
<div class="flex items-center pt-3 pb-3 sticky">
|
||||
<input
|
||||
id="command"
|
||||
v-model="userPrompt"
|
||||
v-focus
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
name="command"
|
||||
:placeholder="`${t(
|
||||
'ai_experiments.generate_or_modify_request_body_input_placeholder'
|
||||
)}`"
|
||||
class="flex flex-1 bg-transparent px-6 text-base text-secondaryDark"
|
||||
/>
|
||||
|
||||
<HoppButtonSecondary
|
||||
:icon="IconArrowRight"
|
||||
class="mr-6 rounded-md"
|
||||
outline
|
||||
filled
|
||||
:loading="isModifyRequestBodyPending"
|
||||
:disabled="!userPrompt || isModifyRequestBodyPending"
|
||||
@click="modifyRequestBody"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<AiexperimentsMergeView
|
||||
:content-left="{
|
||||
content: currentBody ?? '',
|
||||
langMime: 'application/json',
|
||||
}"
|
||||
:content-right="{
|
||||
content: generatedBodyContent,
|
||||
langMime: 'application/json',
|
||||
}"
|
||||
></AiexperimentsMergeView>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex gap-1 px-6 py-3 justify-between items-center w-full">
|
||||
<div
|
||||
v-if="lastTraceID && !submittedFeedback"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<p>{{ t("ai_experiments.feedback_cta_text_long") }}</p>
|
||||
<template v-if="!isSubmitFeedbackPending">
|
||||
<HoppButtonSecondary
|
||||
:icon="IconThumbsUp"
|
||||
outline
|
||||
@click="
|
||||
async () => {
|
||||
if (lastTraceID) {
|
||||
await submitFeedback('positive', lastTraceID)
|
||||
submittedFeedback = true
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
||||
<HoppButtonSecondary
|
||||
:icon="IconThumbsDown"
|
||||
outline
|
||||
@click="submitFeedback('negative', lastTraceID)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<HoppSmartSpinner v-else />
|
||||
</div>
|
||||
|
||||
<div v-if="submittedFeedback">
|
||||
<p>{{ t("ai_experiments.feedback_thank_you") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto space-x-2">
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
@click="
|
||||
() => {
|
||||
emit('closeModal')
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('ai_experiments.accept_change')"
|
||||
outline
|
||||
filled
|
||||
:disabled="isModifyRequestBodyPending || !generatedBodyContent"
|
||||
@click="
|
||||
() => {
|
||||
emit('updateBody', generatedBodyContent)
|
||||
emit('closeModal')
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('request.new')"
|
||||
@close="$emit('hide-modal')"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex gap-1">
|
||||
@@ -30,20 +30,61 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="addRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<div class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="addRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="lastTraceID && !submittedFeedback"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<p>{{ t("ai_experiments.feedback_cta_request_name") }}</p>
|
||||
<template v-if="!isSubmitFeedbackPending">
|
||||
<HoppButtonSecondary
|
||||
:icon="IconThumbsUp"
|
||||
outline
|
||||
@click="
|
||||
async () => {
|
||||
if (lastTraceID) {
|
||||
await submitFeedback('positive', lastTraceID)
|
||||
submittedFeedback = true
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconThumbsDown"
|
||||
outline
|
||||
@click="
|
||||
() => {
|
||||
if (lastTraceID) {
|
||||
submitFeedback('negative', lastTraceID)
|
||||
submittedFeedback = true
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<HoppSmartSpinner />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="submittedFeedback">
|
||||
<p>{{ t("ai_experiments.feedback_thank_you") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
@@ -54,9 +95,14 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useService } from "dioc/vue"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useRequestNameGeneration } from "~/composables/ai-experiments"
|
||||
import {
|
||||
useRequestNameGeneration,
|
||||
useSubmitFeedback,
|
||||
} from "~/composables/ai-experiments"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import IconSparkle from "~icons/lucide/sparkles"
|
||||
import IconThumbsUp from "~icons/lucide/thumbs-up"
|
||||
import IconThumbsDown from "~icons/lucide/thumbs-down"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
@@ -85,8 +131,22 @@ const {
|
||||
generateRequestName,
|
||||
isGenerateRequestNamePending,
|
||||
canDoRequestNameGeneration,
|
||||
lastTraceID,
|
||||
} = useRequestNameGeneration(editingName)
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
submittedFeedback.value = false
|
||||
lastTraceID.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const submittedFeedback = ref(false)
|
||||
const { submitFeedback, isSubmitFeedbackPending } = useSubmitFeedback()
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
watch(
|
||||
() => props.show,
|
||||
|
||||
@@ -30,20 +30,61 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="editRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<div class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="editRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="lastTraceID && !submittedFeedback"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<p>{{ t("ai_experiments.feedback_cta_request_name") }}</p>
|
||||
<template v-if="!isSubmitFeedbackPending">
|
||||
<HoppButtonSecondary
|
||||
:icon="IconThumbsUp"
|
||||
outline
|
||||
@click="
|
||||
async () => {
|
||||
if (lastTraceID) {
|
||||
await submitFeedback('positive', lastTraceID)
|
||||
submittedFeedback = true
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconThumbsDown"
|
||||
outline
|
||||
@click="
|
||||
() => {
|
||||
if (lastTraceID) {
|
||||
submitFeedback('negative', lastTraceID)
|
||||
submittedFeedback = true
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<HoppSmartSpinner />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="submittedFeedback">
|
||||
<p>{{ t("ai_experiments.feedback_thank_you") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
@@ -53,8 +94,14 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useRequestNameGeneration } from "~/composables/ai-experiments"
|
||||
import { ref, watch } from "vue"
|
||||
import {
|
||||
useRequestNameGeneration,
|
||||
useSubmitFeedback,
|
||||
} from "~/composables/ai-experiments"
|
||||
import IconSparkle from "~icons/lucide/sparkles"
|
||||
import IconThumbsUp from "~icons/lucide/thumbs-up"
|
||||
import IconThumbsDown from "~icons/lucide/thumbs-down"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
@@ -85,8 +132,22 @@ const {
|
||||
generateRequestName,
|
||||
canDoRequestNameGeneration,
|
||||
isGenerateRequestNamePending,
|
||||
lastTraceID,
|
||||
} = useRequestNameGeneration(editingName)
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
submittedFeedback.value = false
|
||||
lastTraceID.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const submittedFeedback = ref(false)
|
||||
const { submitFeedback, isSubmitFeedbackPending } = useSubmitFeedback()
|
||||
|
||||
const editRequest = () => {
|
||||
if (editingName.value.trim() === "") {
|
||||
toast.error(t("request.invalid_name"))
|
||||
|
||||
@@ -52,20 +52,61 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
:loading="modalLoadingState"
|
||||
outline
|
||||
@click="saveRequestAs"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<div class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
:loading="modalLoadingState"
|
||||
outline
|
||||
@click="saveRequestAs"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="lastTraceID && !submittedFeedback"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<p>{{ t("ai_experiments.feedback_cta_request_name") }}</p>
|
||||
<template v-if="!isSubmitFeedbackPending">
|
||||
<HoppButtonSecondary
|
||||
:icon="IconThumbsUp"
|
||||
outline
|
||||
@click="
|
||||
async () => {
|
||||
if (lastTraceID) {
|
||||
await submitFeedback('positive', lastTraceID)
|
||||
submittedFeedback = true
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconThumbsDown"
|
||||
outline
|
||||
@click="
|
||||
() => {
|
||||
if (lastTraceID) {
|
||||
submitFeedback('negative', lastTraceID)
|
||||
submittedFeedback = true
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<HoppSmartSpinner />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="submittedFeedback">
|
||||
<p>{{ t("ai_experiments.feedback_thank_you") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
@@ -84,7 +125,10 @@ import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { computed, nextTick, reactive, ref, watch } from "vue"
|
||||
import { useRequestNameGeneration } from "~/composables/ai-experiments"
|
||||
import {
|
||||
useRequestNameGeneration,
|
||||
useSubmitFeedback,
|
||||
} from "~/composables/ai-experiments"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import {
|
||||
createRequestInCollection,
|
||||
@@ -103,6 +147,8 @@ import { GQLTabService } from "~/services/tab/graphql"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { TeamWorkspace } from "~/services/workspace.service"
|
||||
import IconSparkle from "~icons/lucide/sparkles"
|
||||
import IconThumbsDown from "~icons/lucide/thumbs-down"
|
||||
import IconThumbsUp from "~icons/lucide/thumbs-up"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -185,8 +231,22 @@ const {
|
||||
canDoRequestNameGeneration,
|
||||
generateRequestName,
|
||||
isGenerateRequestNamePending,
|
||||
lastTraceID,
|
||||
} = useRequestNameGeneration(requestName)
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
submittedFeedback.value = false
|
||||
lastTraceID.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const submittedFeedback = ref(false)
|
||||
const { submitFeedback, isSubmitFeedbackPending } = useSubmitFeedback()
|
||||
|
||||
watch(
|
||||
() => [RESTTabs.currentActiveTab.value, GQLTabs.currentActiveTab.value],
|
||||
() => {
|
||||
|
||||
@@ -30,19 +30,60 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
outline
|
||||
@click="addRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<div class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
outline
|
||||
@click="addRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="lastTraceID && !submittedFeedback"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<p>{{ t("ai_experiments.feedback_cta_request_name") }}</p>
|
||||
<template v-if="!isSubmitFeedbackPending">
|
||||
<HoppButtonSecondary
|
||||
:icon="IconThumbsUp"
|
||||
outline
|
||||
@click="
|
||||
async () => {
|
||||
if (lastTraceID) {
|
||||
await submitFeedback('positive', lastTraceID)
|
||||
submittedFeedback = true
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconThumbsDown"
|
||||
outline
|
||||
@click="
|
||||
() => {
|
||||
if (lastTraceID) {
|
||||
submitFeedback('negative', lastTraceID)
|
||||
submittedFeedback = true
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<HoppSmartSpinner />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="submittedFeedback">
|
||||
<p>{{ t("ai_experiments.feedback_thank_you") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
@@ -54,9 +95,14 @@ import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useService } from "dioc/vue"
|
||||
import { ref, watch } from "vue"
|
||||
|
||||
import { useRequestNameGeneration } from "~/composables/ai-experiments"
|
||||
import {
|
||||
useRequestNameGeneration,
|
||||
useSubmitFeedback,
|
||||
} from "~/composables/ai-experiments"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
import IconSparkle from "~icons/lucide/sparkles"
|
||||
import IconThumbsUp from "~icons/lucide/thumbs-up"
|
||||
import IconThumbsDown from "~icons/lucide/thumbs-down"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
@@ -86,8 +132,22 @@ const {
|
||||
generateRequestName,
|
||||
isGenerateRequestNamePending,
|
||||
canDoRequestNameGeneration,
|
||||
lastTraceID,
|
||||
} = useRequestNameGeneration(editingName)
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
submittedFeedback.value = false
|
||||
lastTraceID.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const submittedFeedback = ref(false)
|
||||
const { submitFeedback, isSubmitFeedbackPending } = useSubmitFeedback()
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
|
||||
@@ -30,19 +30,60 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
outline
|
||||
@click="saveRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<div class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
outline
|
||||
@click="saveRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.cancel')}`"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="lastTraceID && !submittedFeedback"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<p>{{ t("ai_experiments.feedback_cta_request_name") }}</p>
|
||||
<template v-if="!isSubmitFeedbackPending">
|
||||
<HoppButtonSecondary
|
||||
:icon="IconThumbsUp"
|
||||
outline
|
||||
@click="
|
||||
async () => {
|
||||
if (lastTraceID) {
|
||||
await submitFeedback('positive', lastTraceID)
|
||||
submittedFeedback = true
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconThumbsDown"
|
||||
outline
|
||||
@click="
|
||||
() => {
|
||||
if (lastTraceID) {
|
||||
submitFeedback('negative', lastTraceID)
|
||||
submittedFeedback = true
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<HoppSmartSpinner />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="submittedFeedback">
|
||||
<p>{{ t("ai_experiments.feedback_thank_you") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
@@ -52,9 +93,14 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
||||
import { ref, watch } from "vue"
|
||||
import { useRequestNameGeneration } from "~/composables/ai-experiments"
|
||||
import {
|
||||
useRequestNameGeneration,
|
||||
useSubmitFeedback,
|
||||
} from "~/composables/ai-experiments"
|
||||
import { editGraphqlRequest } from "~/newstore/collections"
|
||||
import IconSparkle from "~icons/lucide/sparkles"
|
||||
import IconThumbsUp from "~icons/lucide/thumbs-up"
|
||||
import IconThumbsDown from "~icons/lucide/thumbs-down"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -85,8 +131,22 @@ const {
|
||||
canDoRequestNameGeneration,
|
||||
generateRequestName,
|
||||
isGenerateRequestNamePending,
|
||||
lastTraceID,
|
||||
} = useRequestNameGeneration(editingName)
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
submittedFeedback.value = false
|
||||
lastTraceID.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const submittedFeedback = ref(false)
|
||||
const { submitFeedback, isSubmitFeedbackPending } = useSubmitFeedback()
|
||||
|
||||
const saveRequest = () => {
|
||||
if (!editingName.value) {
|
||||
toast.error(`${t("collection.invalid_name")}`)
|
||||
|
||||
@@ -42,6 +42,13 @@
|
||||
:icon="prettifyIcon"
|
||||
@click="prettifyRequestBody"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="shouldEnableAIFeatures"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('ai_experiments.modify_with_ai')"
|
||||
:icon="IconSparkles"
|
||||
@click="showModifyBodyModal"
|
||||
/>
|
||||
<label for="payload">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -62,6 +69,13 @@
|
||||
<div class="h-full relative flex flex-col flex-1">
|
||||
<div ref="rawBodyParameters" class="absolute inset-0"></div>
|
||||
</div>
|
||||
|
||||
<AiexperimentsModifyBodyModal
|
||||
v-if="isModifyBodyModalOpen"
|
||||
:current-body="codemirrorValue ?? ''"
|
||||
@close-modal="isModifyBodyModalOpen = false"
|
||||
@update-body="(updatedBody) => (codemirrorValue = updatedBody)"
|
||||
></AiexperimentsModifyBodyModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -73,6 +87,7 @@ import IconFilePlus from "~icons/lucide/file-plus"
|
||||
import IconWand2 from "~icons/lucide/wand-2"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconInfo from "~icons/lucide/info"
|
||||
import IconSparkles from "~icons/lucide/sparkles"
|
||||
import { computed, reactive, Ref, ref, watch } from "vue"
|
||||
import * as TO from "fp-ts/TaskOption"
|
||||
import { pipe } from "fp-ts/function"
|
||||
@@ -90,6 +105,7 @@ import xmlFormat from "xml-formatter"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import * as LJSON from "lossless-json"
|
||||
import { useAIExperiments } from "~/composables/ai-experiments"
|
||||
|
||||
type PossibleContentTypes = Exclude<
|
||||
ValidContentTypes,
|
||||
@@ -203,6 +219,14 @@ const prettifyRequestBody = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const isModifyBodyModalOpen = ref(false)
|
||||
|
||||
const showModifyBodyModal = () => {
|
||||
isModifyBodyModalOpen.value = true
|
||||
}
|
||||
|
||||
const { shouldEnableAIFeatures } = useAIExperiments()
|
||||
|
||||
const prettifyXML = (xml: string) => {
|
||||
return xmlFormat(xml, {
|
||||
indentation: " ",
|
||||
|
||||
@@ -38,6 +38,8 @@ export const useRequestNameGeneration = (targetNameRef: Ref<string>) => {
|
||||
return ENABLE_AI_EXPERIMENTS.value && !!platform.experiments?.aiExperiments
|
||||
})
|
||||
|
||||
const lastTraceID = ref<string | null>(null)
|
||||
|
||||
const generateRequestName = async (
|
||||
requestContext: HoppRESTRequest | HoppGQLRequest | null
|
||||
) => {
|
||||
@@ -65,7 +67,8 @@ export const useRequestNameGeneration = (targetNameRef: Ref<string>) => {
|
||||
return
|
||||
}
|
||||
|
||||
targetNameRef.value = result.right
|
||||
targetNameRef.value = result.right.request_name
|
||||
lastTraceID.value = result.right.trace_id
|
||||
|
||||
isGenerateRequestNamePending.value = false
|
||||
}
|
||||
@@ -74,5 +77,122 @@ export const useRequestNameGeneration = (targetNameRef: Ref<string>) => {
|
||||
generateRequestName,
|
||||
isGenerateRequestNamePending,
|
||||
canDoRequestNameGeneration,
|
||||
lastTraceID,
|
||||
}
|
||||
}
|
||||
|
||||
export const useAIExperiments = () => {
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
const ENABLE_AI_EXPERIMENTS = useSetting("ENABLE_AI_EXPERIMENTS")
|
||||
|
||||
const shouldEnableAIFeatures = computed(() => {
|
||||
// Request generation applies only to the authenticated state
|
||||
if (!currentUser.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
return ENABLE_AI_EXPERIMENTS.value && !!platform.experiments?.aiExperiments
|
||||
})
|
||||
|
||||
return {
|
||||
shouldEnableAIFeatures,
|
||||
}
|
||||
}
|
||||
|
||||
export const useModifyRequestBody = (
|
||||
currentRequestBody: string,
|
||||
userPromptRef: Ref<string>,
|
||||
generatedRequestBodyRef: Ref<string>
|
||||
) => {
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
|
||||
const lastTraceID = ref<string | null>(null)
|
||||
|
||||
const isModifyRequestBodyPending = ref(false)
|
||||
|
||||
const modifyRequestBodyForPlatform =
|
||||
platform.experiments?.aiExperiments?.modifyRequestBody
|
||||
|
||||
const modifyRequestBody = async () => {
|
||||
isModifyRequestBodyPending.value = true
|
||||
|
||||
if (!modifyRequestBodyForPlatform) {
|
||||
toast.error(t("request.modify_request_body_error"))
|
||||
isModifyRequestBodyPending.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const result = await modifyRequestBodyForPlatform(
|
||||
currentRequestBody ?? "",
|
||||
userPromptRef.value
|
||||
)
|
||||
|
||||
if (result && E.isLeft(result)) {
|
||||
toast.error(t("request.modify_request_body_error"))
|
||||
isModifyRequestBodyPending.value = false
|
||||
return
|
||||
}
|
||||
|
||||
generatedRequestBodyRef.value = result.right.modified_body
|
||||
lastTraceID.value = result.right.trace_id
|
||||
|
||||
isModifyRequestBodyPending.value = false
|
||||
return result.right
|
||||
}
|
||||
|
||||
return {
|
||||
modifyRequestBody,
|
||||
isModifyRequestBodyPending,
|
||||
lastTraceID,
|
||||
}
|
||||
}
|
||||
|
||||
export const useSubmitFeedback = () => {
|
||||
const submitFeedbackForPlatform =
|
||||
platform.experiments?.aiExperiments?.submitFeedback
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const isSubmitFeedbackPending = ref(false)
|
||||
|
||||
const submitFeedback = async (
|
||||
rating: "positive" | "negative",
|
||||
traceID: string
|
||||
) => {
|
||||
if (!submitFeedbackForPlatform) {
|
||||
toast.error(t("ai_experiments.feedback_failure"))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
isSubmitFeedbackPending.value = true
|
||||
|
||||
const res = await submitFeedbackForPlatform(
|
||||
rating === "positive" ? 1 : -1,
|
||||
traceID
|
||||
)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
toast.error(t("ai_experiments.feedback_failure"))
|
||||
isSubmitFeedbackPending.value = false
|
||||
return
|
||||
}
|
||||
|
||||
isSubmitFeedbackPending.value = false
|
||||
|
||||
toast.success(t("ai_experiments.feedback_success"))
|
||||
|
||||
return E.right(undefined)
|
||||
}
|
||||
|
||||
return {
|
||||
submitFeedback,
|
||||
isSubmitFeedbackPending,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,30 @@ import * as E from "fp-ts/Either"
|
||||
export type ExperimentsPlatformDef = {
|
||||
aiExperiments?: {
|
||||
enableAIExperiments: boolean
|
||||
generateRequestName: (
|
||||
requestInfo: string
|
||||
) => Promise<E.Either<string, string>>
|
||||
generateRequestName?: (requestInfo: string) => Promise<
|
||||
E.Either<
|
||||
string,
|
||||
{
|
||||
request_name: string
|
||||
trace_id: string
|
||||
}
|
||||
>
|
||||
>
|
||||
modifyRequestBody?: (
|
||||
requestBody: string,
|
||||
userPrompt: string
|
||||
) => Promise<
|
||||
E.Either<
|
||||
string,
|
||||
{
|
||||
modified_body: string
|
||||
trace_id: string
|
||||
}
|
||||
>
|
||||
>
|
||||
submitFeedback?: (
|
||||
rating: -1 | 1,
|
||||
traceID: string
|
||||
) => Promise<E.Either<string, void>>
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user