feat: 添加 Prompt 模板和 Prompt 商店支持 (#268)

* feat: 添加Prompt模板和Prompt商店支持

* feat: well done

---------

Co-authored-by: Redon <790348264@qq.com>
This commit is contained in:
Nothing1024 2023-03-11 16:09:52 +08:00 committed by GitHub
parent 514ab7e9e4
commit 00ade41a76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 525 additions and 11 deletions

View File

@ -0,0 +1,8 @@
[
{
"key": "awesome-chatgpt-prompts-zh",
"desc": "ChatGPT 中文调教指南",
"downloadUrl": "https://raw.githubusercontent.com/Nothing1024/chatgpt-prompt-collection/main/awesome-chatgpt-prompts-zh.json",
"url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh"
}
]

View File

@ -0,0 +1,429 @@
<script setup lang='ts'>
import type { DataTableColumns } from 'naive-ui'
import { computed, h, ref, watch } from 'vue'
import { NButton, NCard, NDataTable, NDivider, NGi, NGrid, NInput, NLayoutContent, NMessageProvider, NModal, NPopconfirm, NSpace, NTabPane, NTabs, useMessage } from 'naive-ui'
import PromptRecommend from '../../../assets/recommend.json'
import { SvgIcon } from '..'
import { usePromptStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
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 { 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('')
// ModalModal
const modalMode = ref('')
// Promptuuidlistitem
const tempModifiedItem = ref<any>({})
// 使Modal, tempPromptKey,
const changeShowModal = (mode: string, 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('已存在重复标题,请重新输入')
return
}
if (i.value === tempPromptValue.value) {
message.error(`已存在重复内容:${tempPromptKey.value},请重新输入`)
return
}
}
promptList.value.unshift({ key: tempPromptKey.value, value: tempPromptValue.value } as never)
message.success('添加 prompt 成功')
changeShowModal('')
}
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('检测修改 Prompt 标题冲突,请重新修改')
return
}
if (i.value === tempPromptValue.value) {
message.error(`检测修改内容${i.key}冲突,请重新修改`)
return
}
}
promptList.value = [{ key: tempPromptKey.value, value: tempPromptValue.value }, ...tempList] as never
message.success('Prompt 信息修改成功')
changeShowModal('')
}
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('删除 Prompt 成功')
}
const clearPromptTemplate = () => {
promptList.value = []
message.success('清空 Prompt 成功')
}
const importPromptTemplate = () => {
try {
const jsonData = JSON.parse(tempPromptValue.value)
for (const i of jsonData) {
let safe = true
for (const j of promptList.value) {
if (j.key === i.key) {
message.warning(`因标题重复跳过:${i.key}`)
safe = false
break
}
if (j.value === i.value) {
message.warning(`因内容重复跳过:${i.key}`)
safe = false
break
}
}
if (safe)
promptList.value.unshift({ key: i.key, value: i.value } as never)
}
message.success('导入成功')
changeShowModal('')
}
catch {
message.error('JSON 格式错误,请检查 JSON 格式')
changeShowModal('')
}
}
//
const exportPromptTemplate = () => {
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)
}
// 线
const downloadPromptTemplate = async () => {
try {
await fetch(downloadURL.value)
.then(response => response.json())
.then((jsonData) => {
tempPromptValue.value = JSON.stringify(jsonData)
}).then(() => {
importPromptTemplate()
})
}
catch {
message.error('网络导入出现问题,请检查网络状态与 JSON 文件有效性')
}
}
//
const renderTemplate = () => {
const [keyLimit, valueLimit] = isMobile.value ? [6, 9] : [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: '标题',
key: 'renderKey',
minWidth: 100,
},
{
title: '内容',
key: 'renderValue',
},
{
title: '操作',
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: () => '修改' },
),
h(
NButton,
{
tertiary: true,
size: 'small',
type: 'error',
onClick: () => deletePromptTemplate(row),
},
{ default: () => '删除' },
),
],
})
},
},
]
}
const columns = createColumns()
watch(
() => promptList,
() => {
promptStore.updatePromptList(promptList.value)
},
{ deep: true },
)
</script>
<template>
<NMessageProvider>
<NModal v-model:show="show" style="width: 90%; max-width: 900px;" preset="card">
<NCard>
<div class="space-y-4">
<NTabs type="segment">
<NTabPane name="local" tab="本地管理">
<NSpace justify="end">
<NButton type="primary" @click="changeShowModal('add')">
添加
</NButton>
<NButton @click="changeShowModal('local_import')">
导入
</NButton>
<NButton @click="exportPromptTemplate()">
导出
</NButton>
<NPopconfirm @positive-click="clearPromptTemplate">
<template #trigger>
<NButton>
清空
</NButton>
</template>
确认是否清空数据?
</NPopconfirm>
</NSpace>
<br>
<NDataTable
:max-height="400"
:columns="columns"
:data="renderTemplate()"
:pagination="pagination"
:bordered="false"
/>
</NTabPane>
<NTabPane name="download" tab="在线导入">
注意请检查下载 JSON 文件来源恶意的JSON文件可能会破坏您的计算机<br><br>
<NGrid x-gap="12" y-gap="12" :cols="24">
<NGi :span="isMobile ? 18 : 22">
<NInput v-model:value="downloadURL" placeholder="请输入正确的 JSON 地址" />
</NGi>
<NGi>
<NButton strong secondary :disabled="downloadDisabled" @click="downloadPromptTemplate()">
下载
</NButton>
</NGi>
</NGrid>
<NDivider />
<NLayoutContent v-if="isMobile" style="height: 360px" content-style=" background:none;" :native-scrollbar="false">
<NCard
v-for="info in promptRecommendList"
:key="info.key" :title="info.key"
style="margin: 5px;"
embedded
:bordered="true"
>
{{ info.desc }}
<template #footer>
<NSpace justify="end">
<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>
</NSpace>
</template>
</NCard>
</NLayoutContent>
<NLayoutContent
v-else
style="height: 360px"
content-style="padding: 10px; background:none;"
:native-scrollbar="false"
>
<NGrid x-gap="12" y-gap="12" :cols="isMobile ? 1 : 3">
<NGi v-for="info in promptRecommendList" :key="info.key">
<NCard :title="info.key" embedded :bordered="true">
{{ info.desc }}
<template #footer>
<NSpace justify="end">
<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>
</NSpace>
</template>
</NCard>
</NGi>
</NGrid>
</NLayoutContent>
</NTabPane>
</NTabs>
</div>
</NCard>
</NModal>
<NModal v-model:show="showModal">
<NCard
style="width: 600px"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<NSpace v-if="modalMode === 'add' || modalMode === 'modify'" vertical>
模板标题
<NInput v-model:value="tempPromptKey" placeholder="搜索" />
模板内容
<NInput v-model:value="tempPromptValue" placeholder="搜索" type="textarea" />
<NButton
strong
secondary
:style="{ width: '100%' }"
:disabled="inputStatus"
@click="() => { modalMode === 'add' ? addPromptTemplate() : modifyPromptTemplate() }"
>
确定
</NButton>
</NSpace>
<NSpace v-if="modalMode === 'local_import'" vertical>
<NInput
v-model:value="tempPromptValue"
placeholder="请粘贴json文件内容"
:autosize="{ minRows: 3, maxRows: 15 }"
type="textarea"
/>
<NButton
strong
secondary
:style="{ width: '100%' }"
:disabled="inputStatus"
@click="() => { importPromptTemplate() }"
>
导入
</NButton>
</NSpace>
</NCard>
</NModal>
</NMessageProvider>
</template>

View File

@ -3,5 +3,6 @@ import NaiveProvider from './NaiveProvider/index.vue'
import SvgIcon from './SvgIcon/index.vue'
import UserAvatar from './UserAvatar/index.vue'
import Setting from './Setting/index.vue'
import PromptStore from './PromptStore/index.vue'
export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting }
export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore }

View File

@ -1,4 +1,5 @@
export * from './app'
export * from './chat'
export * from './user'
export * from './prompt'
export * from './auth'

View File

@ -0,0 +1,18 @@
import { ss } from '@/utils/storage'
const LOCAL_NAME = 'promptStore'
export type PromptList = []
export interface PromptStore {
promptList: PromptList
}
export function getLocalPromptList(): PromptStore {
const promptStore: PromptStore | undefined = ss.get(LOCAL_NAME)
return promptStore ?? { promptList: [] }
}
export function setLocalPromptList(promptStore: PromptStore): void {
ss.set(LOCAL_NAME, promptStore)
}

View File

@ -0,0 +1,17 @@
import { defineStore } from 'pinia'
import type { PromptStore } from './helper'
import { getLocalPromptList, setLocalPromptList } from './helper'
export const usePromptStore = defineStore('prompt-store', {
state: (): PromptStore => getLocalPromptList(),
actions: {
updatePromptList(promptList: []) {
this.$patch({ promptList })
setLocalPromptList({ promptList })
},
getPromptList() {
return this.$state
},
},
})

View File

@ -1,7 +1,8 @@
<script setup lang='ts'>
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { NButton, NInput, useDialog, useMessage } from 'naive-ui'
import { storeToRefs } from 'pinia'
import { NAutoComplete, NButton, NInput, useDialog, useMessage } from 'naive-ui'
import html2canvas from 'html2canvas'
import { Message } from './components'
import { useScroll } from './hooks/useScroll'
@ -11,7 +12,7 @@ import { useUsingContext } from './hooks/useUsingContext'
import HeaderComponent from './components/Header/index.vue'
import { HoverButton, SvgIcon } from '@/components/common'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { useChatStore } from '@/store'
import { useChatStore, usePromptStore } from '@/store'
import { fetchChatAPIProcess } from '@/api'
import { t } from '@/locales'
@ -40,6 +41,11 @@ const conversationList = computed(() => dataSources.value.filter(item => (!item.
const prompt = ref<string>('')
const loading = ref<boolean>(false)
// PromptStore
const promptStore = usePromptStore()
// 使storeToRefsstore
const { promptList: promptTemplate } = storeToRefs<any>(promptStore)
function handleSubmit() {
onConversation()
}
@ -394,6 +400,31 @@ function handleStop() {
}
}
//
// 使valuevalue()
// key,renderOptionvaluerenderLabel
const searchOptions = computed(() => {
if (prompt.value.startsWith('/')) {
return promptTemplate.value.filter((item: { key: string }) => item.key.toLowerCase().includes(prompt.value.substring(1).toLowerCase())).map((obj: { value: any }) => {
return {
label: obj.value,
value: obj.value,
}
})
}
else {
return []
}
})
// valuekey
const renderOption = (option: { label: string }) => {
for (const i of promptTemplate.value) {
if (i.value === option.label)
return [i.key]
}
return []
}
const placeholder = computed(() => {
if (isMobile.value)
return t('chat.placeholderMobile')
@ -490,13 +521,14 @@ onUnmounted(() => {
<SvgIcon icon="ri:chat-history-line" />
</span>
</HoverButton>
<NInput
v-model:value="prompt"
type="textarea"
:autosize="{ minRows: 1, maxRows: 2 }"
:placeholder="placeholder"
@keypress="handleEnter"
/>
<NAutoComplete v-model:value="prompt" :options="searchOptions" :render-label="renderOption">
<template #default="{ handleInput, handleBlur, handleFocus }">
<NInput
v-model:value="prompt" type="textarea" :placeholder="placeholder"
:autosize="{ minRows: 1, maxRows: 2 }" @input="handleInput" @focus="handleFocus" @blur="handleBlur" @keypress="handleEnter"
/>
</template>
</NAutoComplete>
<NButton type="primary" :disabled="buttonDisabled" @click="handleSubmit">
<template #icon>
<span class="dark:text-black">

View File

@ -1,16 +1,18 @@
<script setup lang='ts'>
import type { CSSProperties } from 'vue'
import { computed, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { NButton, NLayoutSider } from 'naive-ui'
import List from './List.vue'
import Footer from './Footer.vue'
import { useAppStore, useChatStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { PromptStore } from '@/components/common'
const appStore = useAppStore()
const chatStore = useChatStore()
const { isMobile } = useBasicLayout()
const show = ref(false)
const collapsed = computed(() => appStore.siderCollapsed)
@ -75,6 +77,11 @@ watch(
<div class="flex-1 min-h-0 pb-4 overflow-hidden">
<List />
</div>
<div class="p-4">
<NButton block @click="show = true">
Prompt Store
</NButton>
</div>
</main>
<Footer />
</div>
@ -82,4 +89,5 @@ watch(
<template v-if="isMobile">
<div v-show="!collapsed" class="fixed inset-0 z-40 bg-black/40" @click="handleUpdateCollapsed" />
</template>
<PromptStore v-model:visible="show" />
</template>