main
vue 221 lines 6.2 KB
Raw
1 <template>
2 <n-collapse-transition :show="!!servers.length">
3 <div class="p-4">
4 <div class="relative flex flex-col overflow-hidden">
5 <div
6 v-if="!!editMessageBody"
7 class="bg-secondary hover:text-primary mb-2 flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-sm transition-colors duration-200"
8 @click="handleCancelEdit()"
9 >
10 <Icon name="carbon:close" :size="20" />
11 <div>Edit message</div>
12 </div>
13 <n-input
14 v-model:value.trim="input"
15 class="max-h-full min-h-20 pt-1 pb-9"
16 type="textarea"
17 placeholder="How can I help you?"
18 :autosize="{
19 minRows: 3,
20 maxRows: 18
21 }"
22 />
23 <div class="absolute right-0 bottom-0 left-0 flex items-center justify-between p-2 pr-3">
24 <div class="flex items-center gap-2.5">
25 <n-select
26 v-model:value="selectedServer"
27 :options="serverOptions"
28 :render-option
29 size="tiny"
30 :consistent-menu-width="false"
31 class="w-auto!"
32 />
33 <n-popover v-if="selectedServer && selectedServerDetails" trigger="hover" class="p-0!">
34 <template #trigger>
35 <Icon name="carbon:help" :size="14" class="cursor-help" />
36 </template>
37 <div class="divide-border flex flex-col divide-y">
38 <div class="flex flex-col gap-1 px-3 py-2 text-sm">
39 <div>{{ selectedServerDetails.name }}</div>
40 <div class="text-secondary text-xs">
41 {{ selectedServerDetails.description }}
42 </div>
43 <div
44 v-if="selectedServerDetails.capabilities.length"
45 class="my-1 flex flex-wrap gap-1"
46 >
47 <n-tag
48 v-for="item of selectedServerDetails.capabilities"
49 :key="item"
50 size="small"
51 class="text-[10px] [&_.n-tag\_\_content]:pb-0.5 [&_.n-tag\_\_content]:leading-0"
52 >
53 {{ item }}
54 </n-tag>
55 </div>
56 </div>
57 </div>
58 </n-popover>
59 <n-popover trigger="hover" class="p-0!">
60 <template #trigger>
61 <Icon name="carbon:settings-adjust" :size="14" />
62 </template>
63 <div class="divide-border flex flex-col divide-y">
64 <div class="px-3 py-2">
65 <div class="flex items-center justify-between gap-4 text-sm">
66 <div>verbose response</div>
67 <n-switch v-model:value="verbose" size="small" />
68 </div>
69 </div>
70 <div class="px-3 py-2">
71 <div class="flex items-center justify-between gap-4 text-sm">
72 <div>show example questions</div>
73 <n-switch v-model:value="showQuestions" size="small" />
74 </div>
75 </div>
76 </div>
77 </n-popover>
78 </div>
79 <n-button
80 circle
81 size="small"
82 secondary
83 :type="isFormValid ? 'primary' : undefined"
84 :disabled="!isFormValid && !loading"
85 @click="loading ? stop() : send()"
86 >
87 <Icon :name="loading ? 'carbon:stop-filled-alt' : 'carbon:arrow-up'" />
88 </n-button>
89 </div>
90 </div>
91 </div>
92 </n-collapse-transition>
93 </template>
94
95 <script setup lang="ts">
96 import type { RemovableRef } from "@vueuse/core"
97 import type { SelectOption } from "naive-ui"
98 import type { VNode } from "vue"
99 import type { MCPServer } from "@/types/copilotMCP.d"
100 import { useStorage } from "@vueuse/core"
101 import _trim from "lodash/trim"
102 import { NButton, NCollapseTransition, NInput, NPopover, NSelect, NSwitch, NTag, NTooltip, useMessage } from "naive-ui"
103 import { computed, h, onBeforeMount, ref, toRefs, watch } from "vue"
104 import Api from "@/api"
105 import Icon from "@/components/common/Icon.vue"
106
107 export interface Message {
108 input: string
109 verbose: boolean
110 server: string
111 }
112
113 const props = defineProps<{ loading?: boolean; editMessageBody?: string | null }>()
114
115 const emit = defineEmits<{
116 (e: "message", value: Message): void
117 (e: "select-server", value: string): void
118 (e: "update-options", value: { verbose: boolean; showQuestions: boolean }): void
119 (e: "stop"): void
120 (e: "server-loaded"): void
121 (e: "cancel-edit"): void
122 }>()
123
124 const { loading, editMessageBody } = toRefs(props)
125 const input = defineModel<string | null>("input", { default: null })
126 const verbose: RemovableRef<boolean> = useStorage<boolean>("ai-chatbot-option-verbose", false, localStorage)
127 const showQuestions: RemovableRef<boolean> = useStorage<boolean>("ai-chatbot-option-questions", true, localStorage)
128 const loadingServers = ref(false)
129 const message = useMessage()
130 const servers = ref<MCPServer[]>([])
131 const selectedServer: RemovableRef<string | null> = useStorage<string | null>(
132 "ai-chatbot-selected-server",
133 null,
134 localStorage
135 )
136 const serverOptions = computed(() => servers.value.map(o => ({ ...o, label: o.name })))
137 const selectedServerDetails = computed(() => servers.value.find(o => o.value === selectedServer.value))
138 const isFormValid = computed(() => !!_trim(input.value || ""))
139
140 function renderOption({ node, option }: { node: VNode; option: SelectOption }) {
141 return h(
142 NTooltip,
143 { placement: "right", class: "max-w-40 text-xs!" },
144 {
145 trigger: () => node,
146 default: () => option.description
147 }
148 )
149 }
150
151 function getList() {
152 loadingServers.value = true
153
154 Api.copilotMCP
155 .getAvailableServers()
156 .then(res => {
157 if (res.data.success) {
158 servers.value = res.data?.servers || []
159 if (servers.value.length && !selectedServer.value) {
160 selectedServer.value = servers.value[0]?.value ?? null
161 }
162 emit("server-loaded")
163 } else {
164 message.warning(res.data?.message || "An error occurred. Please try again later.")
165 }
166 })
167 .catch(err => {
168 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
169 })
170 .finally(() => {
171 loadingServers.value = false
172 })
173 }
174
175 function reset() {
176 input.value = null
177 }
178
179 function send() {
180 if (input.value && selectedServer.value) {
181 emit("message", {
182 input: input.value,
183 server: selectedServer.value,
184 verbose: verbose.value
185 })
186
187 reset()
188 }
189 }
190
191 function stop() {
192 emit("stop")
193 }
194
195 function handleCancelEdit() {
196 emit("cancel-edit")
197 reset()
198 }
199
200 watch(
201 selectedServer,
202 val => {
203 if (val) {
204 emit("select-server", val)
205 }
206 },
207 { immediate: true }
208 )
209
210 watch(
211 [verbose, showQuestions],
212 () => {
213 emit("update-options", { verbose: verbose.value, showQuestions: showQuestions.value })
214 },
215 { immediate: true }
216 )
217
218 onBeforeMount(() => {
219 getList()
220 })
221 </script>