Spaces:
Runtime error
Runtime error
<script setup lang='ts'> | |
import type { DataTableColumns } from 'naive-ui' | |
import { computed, h, ref, watch } from 'vue' | |
import { NButton, NCard, NDataTable, NDivider, NInput, NList, NListItem, NModal, NPopconfirm, NSpace, NTabPane, NTabs, NThing, useMessage } from 'naive-ui' | |
import PromptRecommend from '../../../assets/recommend.json' | |
import { SvgIcon } from '..' | |
import { usePromptStore } from '@/store' | |
import { useBasicLayout } from '@/hooks/useBasicLayout' | |
import { t } from '@/locales' | |
interface DataProps { | |
renderKey: string | |
renderValue: string | |
key: string | |
value: string | |
} | |
interface Props { | |
visible: boolean | |
} | |
interface Emit { | |
(e: 'update:visible', visible: boolean): void | |
} | |
const props = defineProps<Props>() | |
const emit = defineEmits<Emit>() | |
const message = useMessage() | |
const show = computed({ | |
get: () => props.visible, | |
set: (visible: boolean) => emit('update:visible', visible), | |
}) | |
const showModal = ref(false) | |
const importLoading = ref(false) | |
const exportLoading = ref(false) | |
const searchValue = ref<string>('') | |
// 移动端自适应相关 | |
const { isMobile } = useBasicLayout() | |
const promptStore = usePromptStore() | |
// Prompt在线导入推荐List,根据部署者喜好进行修改(assets/recommend.json) | |
const promptRecommendList = PromptRecommend | |
const promptList = ref<any>(promptStore.promptList) | |
// 用于添加修改的临时prompt参数 | |
const tempPromptKey = ref('') | |
const tempPromptValue = ref('') | |
// Modal模式,根据不同模式渲染不同的Modal内容 | |
const modalMode = ref('') | |
// 这个是为了后期的修改Prompt内容考虑,因为要针对无uuid的list进行修改,且考虑到不能出现标题和内容的冲突,所以就需要一个临时item来记录一下 | |
const tempModifiedItem = ref<any>({}) | |
// 添加修改导入都使用一个Modal, 临时修改内容占用tempPromptKey,切换状态前先将内容都清楚 | |
const changeShowModal = (mode: 'add' | 'modify' | 'local_import', selected = { key: '', value: '' }) => { | |
if (mode === 'add') { | |
tempPromptKey.value = '' | |
tempPromptValue.value = '' | |
} | |
else if (mode === 'modify') { | |
tempModifiedItem.value = { ...selected } | |
tempPromptKey.value = selected.key | |
tempPromptValue.value = selected.value | |
} | |
else if (mode === 'local_import') { | |
tempPromptKey.value = 'local_import' | |
tempPromptValue.value = '' | |
} | |
showModal.value = !showModal.value | |
modalMode.value = mode | |
} | |
// 在线导入相关 | |
const downloadURL = ref('') | |
const downloadDisabled = computed(() => downloadURL.value.trim().length < 1) | |
const setDownloadURL = (url: string) => { | |
downloadURL.value = url | |
} | |
// 控制 input 按钮 | |
const inputStatus = computed (() => tempPromptKey.value.trim().length < 1 || tempPromptValue.value.trim().length < 1) | |
// Prompt模板相关操作 | |
const addPromptTemplate = () => { | |
for (const i of promptList.value) { | |
if (i.key === tempPromptKey.value) { | |
message.error(t('store.addRepeatTitleTips')) | |
return | |
} | |
if (i.value === tempPromptValue.value) { | |
message.error(t('store.addRepeatContentTips', { msg: tempPromptKey.value })) | |
return | |
} | |
} | |
promptList.value.unshift({ key: tempPromptKey.value, value: tempPromptValue.value } as never) | |
message.success(t('common.addSuccess')) | |
changeShowModal('add') | |
} | |
const modifyPromptTemplate = () => { | |
let index = 0 | |
// 通过临时索引把待修改项摘出来 | |
for (const i of promptList.value) { | |
if (i.key === tempModifiedItem.value.key && i.value === tempModifiedItem.value.value) | |
break | |
index = index + 1 | |
} | |
const tempList = promptList.value.filter((_: any, i: number) => i !== index) | |
// 搜索有冲突的部分 | |
for (const i of tempList) { | |
if (i.key === tempPromptKey.value) { | |
message.error(t('store.editRepeatTitleTips')) | |
return | |
} | |
if (i.value === tempPromptValue.value) { | |
message.error(t('store.editRepeatContentTips', { msg: i.key })) | |
return | |
} | |
} | |
promptList.value = [{ key: tempPromptKey.value, value: tempPromptValue.value }, ...tempList] as never | |
message.success(t('common.editSuccess')) | |
changeShowModal('modify') | |
} | |
const deletePromptTemplate = (row: { key: string; value: string }) => { | |
promptList.value = [ | |
...promptList.value.filter((item: { key: string; value: string }) => item.key !== row.key), | |
] as never | |
message.success(t('common.deleteSuccess')) | |
} | |
const clearPromptTemplate = () => { | |
promptList.value = [] | |
message.success(t('common.clearSuccess')) | |
} | |
const importPromptTemplate = (from = 'online') => { | |
try { | |
const jsonData = JSON.parse(tempPromptValue.value) | |
let key = '' | |
let value = '' | |
// 可以扩展加入更多模板字典的key | |
if ('key' in jsonData[0]) { | |
key = 'key' | |
value = 'value' | |
} | |
else if ('act' in jsonData[0]) { | |
key = 'act' | |
value = 'prompt' | |
} | |
else { | |
// 不支持的字典的key防止导入 以免破坏prompt商店打开 | |
message.warning('prompt key not supported.') | |
throw new Error('prompt key not supported.') | |
} | |
for (const i of jsonData) { | |
if (!(key in i) || !(value in i)) | |
throw new Error(t('store.importError')) | |
let safe = true | |
for (const j of promptList.value) { | |
if (j.key === i[key]) { | |
message.warning(t('store.importRepeatTitle', { msg: i[key] })) | |
safe = false | |
break | |
} | |
if (j.value === i[value]) { | |
message.warning(t('store.importRepeatContent', { msg: i[key] })) | |
safe = false | |
break | |
} | |
} | |
if (safe) | |
promptList.value.unshift({ key: i[key], value: i[value] } as never) | |
} | |
message.success(t('common.importSuccess')) | |
} | |
catch { | |
message.error('JSON 格式错误,请检查 JSON 格式') | |
} | |
if (from === 'local') | |
showModal.value = !showModal.value | |
} | |
// 模板导出 | |
const exportPromptTemplate = () => { | |
exportLoading.value = true | |
const jsonDataStr = JSON.stringify(promptList.value) | |
const blob = new Blob([jsonDataStr], { type: 'application/json' }) | |
const url = URL.createObjectURL(blob) | |
const link = document.createElement('a') | |
link.href = url | |
link.download = 'ChatGPTPromptTemplate.json' | |
link.click() | |
URL.revokeObjectURL(url) | |
exportLoading.value = false | |
} | |
// 模板在线导入 | |
const downloadPromptTemplate = async () => { | |
try { | |
importLoading.value = true | |
const response = await fetch(downloadURL.value) | |
const jsonData = await response.json() | |
if ('key' in jsonData[0] && 'value' in jsonData[0]) | |
tempPromptValue.value = JSON.stringify(jsonData) | |
if ('act' in jsonData[0] && 'prompt' in jsonData[0]) { | |
const newJsonData = jsonData.map((item: { act: string; prompt: string }) => { | |
return { | |
key: item.act, | |
value: item.prompt, | |
} | |
}) | |
tempPromptValue.value = JSON.stringify(newJsonData) | |
} | |
importPromptTemplate() | |
downloadURL.value = '' | |
} | |
catch { | |
message.error(t('store.downloadError')) | |
downloadURL.value = '' | |
} | |
finally { | |
importLoading.value = false | |
} | |
} | |
// 移动端自适应相关 | |
const renderTemplate = () => { | |
const [keyLimit, valueLimit] = isMobile.value ? [10, 30] : [15, 50] | |
return promptList.value.map((item: { key: string; value: string }) => { | |
return { | |
renderKey: item.key.length <= keyLimit ? item.key : `${item.key.substring(0, keyLimit)}...`, | |
renderValue: item.value.length <= valueLimit ? item.value : `${item.value.substring(0, valueLimit)}...`, | |
key: item.key, | |
value: item.value, | |
} | |
}) | |
} | |
const pagination = computed(() => { | |
const [pageSize, pageSlot] = isMobile.value ? [6, 5] : [7, 15] | |
return { | |
pageSize, pageSlot, | |
} | |
}) | |
// table相关 | |
const createColumns = (): DataTableColumns<DataProps> => { | |
return [ | |
{ | |
title: t('store.title'), | |
key: 'renderKey', | |
}, | |
{ | |
title: t('store.description'), | |
key: 'renderValue', | |
}, | |
{ | |
title: t('common.action'), | |
key: 'actions', | |
width: 100, | |
align: 'center', | |
render(row) { | |
return h('div', { class: 'flex items-center flex-col gap-2' }, { | |
default: () => [h( | |
NButton, | |
{ | |
tertiary: true, | |
size: 'small', | |
type: 'info', | |
onClick: () => changeShowModal('modify', row), | |
}, | |
{ default: () => t('common.edit') }, | |
), | |
h( | |
NButton, | |
{ | |
tertiary: true, | |
size: 'small', | |
type: 'error', | |
onClick: () => deletePromptTemplate(row), | |
}, | |
{ default: () => t('common.delete') }, | |
), | |
], | |
}) | |
}, | |
}, | |
] | |
} | |
const columns = createColumns() | |
watch( | |
() => promptList, | |
() => { | |
promptStore.updatePromptList(promptList.value) | |
}, | |
{ deep: true }, | |
) | |
const dataSource = computed(() => { | |
const data = renderTemplate() | |
const value = searchValue.value | |
if (value && value !== '') { | |
return data.filter((item: DataProps) => { | |
return item.renderKey.includes(value) || item.renderValue.includes(value) | |
}) | |
} | |
return data | |
}) | |
</script> | |
<template> | |
<NModal v-model:show="show" style="width: 90%; max-width: 900px;" preset="card"> | |
<div class="space-y-4"> | |
<NTabs type="segment"> | |
<NTabPane name="local" :tab="$t('store.local')"> | |
<div | |
class="flex gap-3 mb-4" | |
:class="[isMobile ? 'flex-col' : 'flex-row justify-between']" | |
> | |
<div class="flex items-center space-x-4"> | |
<NButton | |
type="primary" | |
size="small" | |
@click="changeShowModal('add')" | |
> | |
{{ $t('common.add') }} | |
</NButton> | |
<NButton | |
size="small" | |
@click="changeShowModal('local_import')" | |
> | |
{{ $t('common.import') }} | |
</NButton> | |
<NButton | |
size="small" | |
:loading="exportLoading" | |
@click="exportPromptTemplate()" | |
> | |
{{ $t('common.export') }} | |
</NButton> | |
<NPopconfirm @positive-click="clearPromptTemplate"> | |
<template #trigger> | |
<NButton size="small"> | |
{{ $t('common.clear') }} | |
</NButton> | |
</template> | |
{{ $t('store.clearStoreConfirm') }} | |
</NPopconfirm> | |
</div> | |
<div class="flex items-center"> | |
<NInput v-model:value="searchValue" style="width: 100%" /> | |
</div> | |
</div> | |
<NDataTable | |
v-if="!isMobile" | |
:max-height="400" | |
:columns="columns" | |
:data="dataSource" | |
:pagination="pagination" | |
:bordered="false" | |
/> | |
<NList v-if="isMobile" style="max-height: 400px; overflow-y: auto;"> | |
<NListItem v-for="(item, index) of dataSource" :key="index"> | |
<NThing :title="item.renderKey" :description="item.renderValue" /> | |
<template #suffix> | |
<div class="flex flex-col items-center gap-2"> | |
<NButton tertiary size="small" type="info" @click="changeShowModal('modify', item)"> | |
{{ t('common.edit') }} | |
</NButton> | |
<NButton tertiary size="small" type="error" @click="deletePromptTemplate(item)"> | |
{{ t('common.delete') }} | |
</NButton> | |
</div> | |
</template> | |
</NListItem> | |
</NList> | |
</NTabPane> | |
<NTabPane name="download" :tab="$t('store.online')"> | |
<p class="mb-4"> | |
{{ $t('store.onlineImportWarning') }} | |
</p> | |
<div class="flex items-center gap-4"> | |
<NInput v-model:value="downloadURL" placeholder="" /> | |
<NButton | |
strong | |
secondary | |
:disabled="downloadDisabled" | |
:loading="importLoading" | |
@click="downloadPromptTemplate()" | |
> | |
{{ $t('common.download') }} | |
</NButton> | |
</div> | |
<NDivider /> | |
<div class="max-h-[360px] overflow-y-auto space-y-4"> | |
<NCard | |
v-for="info in promptRecommendList" | |
:key="info.key" :title="info.key" | |
:bordered="true" | |
embedded | |
> | |
<p | |
class="overflow-hidden text-ellipsis whitespace-nowrap" | |
:title="info.desc" | |
> | |
{{ info.desc }} | |
</p> | |
<template #footer> | |
<div class="flex items-center justify-end space-x-4"> | |
<NButton text> | |
<a | |
:href="info.url" | |
target="_blank" | |
> | |
<SvgIcon class="text-xl" icon="ri:link" /> | |
</a> | |
</NButton> | |
<NButton text @click="setDownloadURL(info.downloadUrl) "> | |
<SvgIcon class="text-xl" icon="ri:add-fill" /> | |
</NButton> | |
</div> | |
</template> | |
</NCard> | |
</div> | |
</NTabPane> | |
</NTabs> | |
</div> | |
</NModal> | |
<NModal v-model:show="showModal" style="width: 90%; max-width: 600px;" preset="card"> | |
<NSpace v-if="modalMode === 'add' || modalMode === 'modify'" vertical> | |
{{ t('store.title') }} | |
<NInput v-model:value="tempPromptKey" /> | |
{{ t('store.description') }} | |
<NInput v-model:value="tempPromptValue" type="textarea" /> | |
<NButton | |
block | |
type="primary" | |
:disabled="inputStatus" | |
@click="() => { modalMode === 'add' ? addPromptTemplate() : modifyPromptTemplate() }" | |
> | |
{{ t('common.confirm') }} | |
</NButton> | |
</NSpace> | |
<NSpace v-if="modalMode === 'local_import'" vertical> | |
<NInput | |
v-model:value="tempPromptValue" | |
:placeholder="t('store.importPlaceholder')" | |
:autosize="{ minRows: 3, maxRows: 15 }" | |
type="textarea" | |
/> | |
<NButton | |
block | |
type="primary" | |
:disabled="inputStatus" | |
@click="() => { importPromptTemplate('local') }" | |
> | |
{{ t('common.import') }} | |
</NButton> | |
</NSpace> | |
</NModal> | |
</template> | |