ConfigDxtCard.vue
1 <script setup lang="ts"> 2 import { ref, watchEffect, reactive } from 'vue' 3 import { McpbUserConfigValues } from '@anthropic-ai/mcpb' 4 import { getDxtUrl, openDxtFilePath } from '@/renderer/utils' 5 import { useDxtStore, validateNumberRange } from '@/renderer/store/dxt' 6 import type { McpMetadataDxt, userConfigValue, McpbManifestAny, McpDxtErrors } from '@/types/mcp' 7 import { useI18n } from 'vue-i18n' 8 import MarkdownCard from '@/renderer/components/common/MarkdownCard.vue' 9 const { t } = useI18n() 10 11 type McpbUserConfigurationOption = McpbUserConfigValues 12 13 const dxtStore = useDxtStore() 14 15 const iconPrefix = ref('') 16 17 watchEffect(() => { 18 getDxtUrl() 19 .then((url) => { 20 iconPrefix.value = url 21 }) 22 .catch((error) => { 23 console.error('Failed to fetch icon prefix:', error) 24 iconPrefix.value = '' 25 }) 26 }) 27 28 const props = defineProps({ 29 modelValue: { 30 type: Object as () => McpMetadataDxt, 31 required: true 32 } 33 }) 34 35 const { modelValue: metadata } = props 36 const { config: manifest } = metadata 37 38 const showPassword = reactive<Record<string, boolean>>({}) 39 40 function toggleShowPassword(key: string) { 41 showPassword[key] = !showPassword[key] 42 } 43 44 const normalizeIconPath = (icon: string) => { 45 return icon.replace(/^\.\/?|\//, '') 46 } 47 48 const getIcon = (metadata: McpMetadataDxt) => { 49 if (!metadata.config || hasErrors(metadata.config)) { 50 return 51 } else { 52 const icon = metadata.config.icon 53 if (!icon) { 54 return 55 } else { 56 return `${iconPrefix.value}/${metadata.name}/${normalizeIconPath(icon)}` 57 } 58 } 59 } 60 61 const dynamicModel = (name: string, key: string) => ({ 62 get: () => dxtStore.getConfigAttribute(name, key), 63 set: (val: userConfigValue) => dxtStore.updateConfigAttribute(name, key, val) 64 }) 65 66 function getPlatformColor(platform: string): string { 67 switch (platform) { 68 case 'darwin': 69 return 'grey-darken-2' 70 case 'win32': 71 return 'blue-darken-2' 72 case 'linux': 73 return 'orange-darken-4' 74 default: 75 return 'grey' 76 } 77 } 78 79 function getPlatformIcon(platform: string): string { 80 switch (platform) { 81 case 'darwin': 82 return 'mdi-apple' 83 case 'win32': 84 return 'mdi-microsoft-windows' 85 case 'linux': 86 return 'mdi-linux' 87 default: 88 return 'mdi-help-circle' 89 } 90 } 91 92 const getErrorState = (para: McpbUserConfigurationOption, value: any): boolean | undefined => { 93 if (!para.required) { 94 return undefined 95 } 96 97 const isEmptyArray = Array.isArray(value) && value.length === 0 98 return !value || isEmptyArray 99 } 100 101 const getErrorMessages = (para: McpbUserConfigurationOption, key: string) => { 102 const value = dynamicModel(metadata.name, key).get() 103 return getErrorState(para, value) ? [t('dxt.required')] : [] 104 } 105 106 function hasErrors(config: McpbManifestAny | McpDxtErrors): config is McpDxtErrors { 107 return 'errors' in config && Array.isArray(config.errors) 108 } 109 </script> 110 111 <template> 112 <div v-if="hasErrors(manifest)"> 113 <v-card 114 v-for="error in manifest.errors" 115 :key="error.field" 116 :subtitle="error.field" 117 :text="error.message" 118 color="error" 119 > 120 </v-card> 121 </div> 122 <div v-else> 123 <v-card :title="$t('dxt.title')" :subtitle="metadata.name"> 124 <template #append> 125 <v-chip size="small" class="font-weight-bold" color="primary"> 126 {{ manifest.version }} 127 </v-chip> 128 </template> 129 130 <v-divider></v-divider> 131 <v-card 132 class="mx-auto" 133 variant="text" 134 :subtitle="manifest.description" 135 :title="manifest.display_name" 136 > 137 <template #prepend> 138 <v-avatar class="mr-2" rounded="lg" size="48"> 139 <v-img :src="getIcon(metadata)"></v-img> 140 </v-avatar> 141 </template> 142 <template #append> 143 <v-btn 144 color="primary" 145 variant="text" 146 rounded="lg" 147 icon="mdi-folder-open" 148 @click="openDxtFilePath(metadata.name)" 149 ></v-btn> 150 </template> 151 <v-card-text v-if="manifest.long_description"> 152 <MarkdownCard :model-value="manifest.long_description"></MarkdownCard> 153 </v-card-text> 154 </v-card> 155 </v-card> 156 <v-card :title="$t('dxt.user-config')" class="mt-4"> 157 <v-divider></v-divider> 158 <v-card-text> 159 <v-row v-for="(para, key) in manifest.user_config" :key="key" class="mx-3 mt-4 mb-0"> 160 <v-text-field 161 v-if="para.sensitive === true" 162 prepend-icon="mdi-key-variant" 163 :type="showPassword[key] ? 'text' : 'password'" 164 :label="para.title" 165 density="compact" 166 variant="outlined" 167 :placeholder="para.default?.toString()" 168 persistent-placeholder 169 :append-inner-icon="showPassword[key] ? 'mdi-eye-off' : 'mdi-eye'" 170 :model-value="dynamicModel(metadata.name, key).get()" 171 clearable 172 :error="getErrorState(para, key)" 173 :error-messages="getErrorMessages(para, key)" 174 @click:append-inner="toggleShowPassword(key)" 175 @update:model-value="dynamicModel(metadata.name, key).set($event)" 176 > 177 </v-text-field> 178 <v-number-input 179 v-else-if="para.type === 'number'" 180 prepend-icon="mdi-numeric" 181 :model-value="dynamicModel(metadata.name, key).get() as number" 182 :label="para.title" 183 density="compact" 184 variant="outlined" 185 :placeholder="para.default?.toString()" 186 persistent-placeholder 187 :max="para.max" 188 :min="para.min" 189 :hint="validateNumberRange(para.min, para.max)" 190 clearable 191 control-variant="stacked" 192 inset 193 :error="getErrorState(para, key)" 194 :error-messages="getErrorMessages(para, key)" 195 @update:model-value="dynamicModel(metadata.name, key).set($event)" 196 ></v-number-input> 197 <v-combobox 198 v-else-if="para.type === 'directory' || para.type === 'file'" 199 chips 200 clearable 201 :prepend-icon=" 202 para.type === 'directory' ? 'mdi-folder-open-outline' : 'mdi-file-outline' 203 " 204 :multiple="para.multiple" 205 variant="outlined" 206 density="compact" 207 :label="para.title" 208 :placeholder="para.default?.toString()" 209 persistent-placeholder 210 :model-value="dynamicModel(metadata.name, key).get()" 211 :error="getErrorState(para, key)" 212 :error-messages="getErrorMessages(para, key)" 213 @update:model-value="dynamicModel(metadata.name, key).set($event)" 214 ></v-combobox> 215 216 <v-text-field 217 v-else 218 prepend-icon="mdi-alphabetical" 219 :model-value="dynamicModel(metadata.name, key).get()" 220 :label="para.title" 221 density="compact" 222 variant="outlined" 223 :placeholder="para.default?.toString()" 224 persistent-placeholder 225 clearable 226 :error="getErrorState(para, key)" 227 :error-messages="getErrorMessages(para, key)" 228 @update:model-value="dynamicModel(metadata.name, key).set($event)" 229 ></v-text-field> 230 </v-row> 231 </v-card-text> 232 </v-card> 233 234 <v-card :title="$t('dxt.description')" class="mt-4"> 235 <v-divider></v-divider> 236 <v-card-text> 237 <v-row v-if="manifest.compatibility?.platforms" class="mx-2 mt-2 mb-4"> 238 <div class="d-flex align-center ga-4"> 239 <v-label style="width: 80px">{{ $t('dxt.platform') }}</v-label> 240 <div v-for="platform in manifest.compatibility.platforms" :key="platform"> 241 <v-chip :color="getPlatformColor(platform)" label size="small"> 242 <v-icon :icon="getPlatformIcon(platform)" start></v-icon> 243 {{ platform }} 244 </v-chip> 245 </div> 246 </div> 247 </v-row> 248 <v-row v-if="manifest.keywords" class="mx-2 mt-2 mb-4"> 249 <div class="d-flex align-center ga-4"> 250 <v-label style="width: 80px">{{ $t('dxt.keywords') }}</v-label> 251 <v-chip 252 v-for="keyword in manifest.keywords" 253 :key="keyword" 254 color="light-green-darken-4" 255 size="small" 256 > 257 {{ keyword }} 258 </v-chip> 259 </div> 260 </v-row> 261 262 <v-row class="mx-1 mt-2 mb-2" density="compact"> 263 <v-col v-if="manifest.author" cols="12" md="6"> 264 <v-card 265 color="indigo-lighten-2" 266 append-icon="mdi-open-in-new" 267 class="mx-auto" 268 :href="manifest.author.url" 269 prepend-icon="mdi-account" 270 :subtitle="manifest.author.url || manifest.author.email" 271 target="_blank" 272 :title="manifest.author.name" 273 ></v-card> 274 </v-col> 275 <v-col v-if="manifest.repository" cols="12" md="6"> 276 <v-card 277 color="green-lighten-1" 278 append-icon="mdi-open-in-new" 279 class="mx-auto" 280 :href="manifest.repository.url" 281 prepend-icon="mdi-github" 282 :subtitle="manifest.repository.url" 283 target="_blank" 284 :title="$t('dxt.repository') + ' - ' + manifest.repository.type" 285 ></v-card> 286 </v-col> 287 <v-col v-if="manifest.homepage" cols="12" md="6"> 288 <v-card 289 color="blue-lighten-1" 290 append-icon="mdi-open-in-new" 291 class="mx-auto" 292 :href="manifest.homepage" 293 prepend-icon="mdi-home" 294 :subtitle="manifest.homepage" 295 target="_blank" 296 :title="$t('dxt.homepage')" 297 ></v-card> 298 </v-col> 299 <v-col v-if="manifest.documentation" cols="12" md="6"> 300 <v-card 301 color="brown-lighten-2" 302 append-icon="mdi-open-in-new" 303 class="mx-auto" 304 :href="manifest.documentation" 305 prepend-icon="mdi-file-document" 306 :subtitle="manifest.documentation" 307 target="_blank" 308 :title="$t('dxt.documentation')" 309 ></v-card> 310 </v-col> 311 <v-col v-if="manifest.support" cols="12" md="6"> 312 <v-card 313 color="red-lighten-2" 314 append-icon="mdi-open-in-new" 315 class="mx-auto" 316 :href="manifest.support" 317 prepend-icon="mdi-alert-circle-outline" 318 :subtitle="manifest.support" 319 target="_blank" 320 :title="$t('dxt.support')" 321 ></v-card> 322 </v-col> 323 </v-row> 324 </v-card-text> 325 </v-card> 326 327 <v-card v-if="manifest.tools" :title="$t('dxt.tools')" class="mt-4"> 328 <v-divider></v-divider> 329 <v-card-text> 330 <v-data-table 331 hover 332 hide-default-footer 333 hide-default-header 334 hide-no-data 335 disable-sort 336 :items-per-page="-1" 337 :items="manifest.tools" 338 ></v-data-table> 339 </v-card-text> 340 </v-card> 341 </div> 342 </template> 343 <style scoped> 344 .wrap-text { 345 white-space: pre-line; 346 overflow-wrap: break-word; 347 } 348 </style>