/ src / renderer / components / common / ConfigDxtCard.vue
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>