feat(component): 新增GPT欢迎页面,完善设置页面

This commit is contained in:
nongyehong 2024-07-02 01:03:14 +08:00
parent 058b91d7af
commit 9b771e02ec
12 changed files with 420 additions and 84 deletions

View File

@ -7,7 +7,7 @@
<title>HuLa</title>
<!--引入iconpark图标库-->
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_95.0ab6745fae3ccd4f96f945bab1f8bd0d.js"></script>
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_97.8e01ce86874358e3d6e87829c9ed23c6.js"></script>
</head>
<body>

View File

@ -115,7 +115,7 @@ export const useChatMain = (activeItem?: SessionItem) => {
...commonMenuList.value,
{
label: '另存为',
icon: 'download',
icon: 'Importing',
click: (item: any) => {
console.log(item)
}
@ -142,7 +142,7 @@ export const useChatMain = (activeItem?: SessionItem) => {
...commonMenuList.value,
{
label: '另存为',
icon: 'download',
icon: 'Importing',
click: (item: any) => {
console.log(item)
}

View File

@ -72,7 +72,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
})
}
},
{ label: '另存为', icon: 'download', disabled: true },
{ label: '另存为', icon: 'Importing', disabled: true },
{ label: '全部选择', icon: 'check-one' }
])

View File

@ -46,6 +46,11 @@ const routes: Array<RouteRecordRaw> = [
name: 'robot',
component: () => import('@/views/home-window/robot/index.vue'),
children: [
{
path: '/welcome',
name: 'welcome',
component: () => import('@/views/home-window/robot/views/Welcome.vue')
},
{
path: '/chat',
name: 'chat',

View File

@ -75,7 +75,7 @@
}
/** emoji回复气泡的样式 */
.emoji-reply-bubble {
@apply relative rounded-50px p-[4px_8px] cursor-pointer select-none bg-#13987F66 text-14px w-fit border-(1px solid #13987F);
@apply relative rounded-50px p-[4px_8px] cursor-pointer select-none bg-#13987F66 text-14px w-fit border-(1px solid #13987F) shadow-md;
}
/** 跳转到回复内容时候显示的样式 */
.active-reply {

View File

@ -199,6 +199,26 @@ const add = () => {
const deleteChat = (item: any) => {
// keyitems
const index = chatList.value.indexOf(item)
/**
* 删除最后一个元素后触发新增元素的函数
* @param isActive 是否选中新增的元素
*/
function triggeringAdd(isActive?: boolean) {
if (chatList.value.length === 0) {
nextTick(() => {
add()
//
if (isActive) {
handleActive(chatList.value[0])
window.$message.success(`已删除 ${item.title}`, {
icon: () => h(NIcon, null, { default: () => h('svg', null, [h('use', { href: '#face' })]) })
})
}
})
}
}
//
if (index !== -1) {
const removeItem = chatList.value.splice(index, 1)[0]
@ -208,22 +228,15 @@ const deleteChat = (item: any) => {
activeItem.value = chatList.value[index].id
handleActive(chatList.value[index])
} else {
//
if (chatList.value.length === 0) {
nextTick(() => {
add()
//
handleActive(chatList.value[0])
window.$message.success(`已删除 ${item.title}`, {
icon: () => h(NIcon, null, { default: () => h('svg', null, [h('use', { href: '#face' })]) })
})
})
}
// chatList,
triggeringAdd(true)
//
activeItem.value = chatList.value[chatList.value.length - 1].id
handleActive(chatList.value[chatList.value.length - 1])
}
}
//
triggeringAdd()
window.$message.success(`已删除 ${item.title}`, {
icon: () => h(NIcon, null, { default: () => h('svg', null, [h('use', { href: '#face' })]) })
})
@ -231,8 +244,10 @@ const deleteChat = (item: any) => {
}
onMounted(() => {
/** 默认选择第一个聊天内容 */
handleActive(chatList.value[0])
// /** */
// handleActive(chatList.value[0])
/** 刚加载的时候默认跳转到欢迎页面 */
router.push('/welcome')
Mitt.on('update-chat-title', (e) => {
chatList.value.filter((item) => {
if (item.id === e.id) {

View File

@ -1,5 +1,6 @@
<template>
<!-- 主体内容 -->
<!-- // TODO 使 (nyh -> 2024-07-01 10:44:14)-->
<main>
<div class="flex truncate p-[14px_20px] justify-between items-center gap-50px">
<n-flex :size="10" vertical class="truncate">

View File

@ -0,0 +1,100 @@
<template>
<n-flex vertical :size="50" align="center" justify="center" class="flex flex-1">
<!-- logo -->
<img class="w-275px h-125px drop-shadow-2xl" src="@/assets/logo/hula.png" alt="" />
<n-flex vertical justify="center" :size="16" class="p-[40px_20px]">
<p class="text-(14px [--chat-text-color])">你可以尝试使用以下功能</p>
<n-flex align="center" :size="16">
<n-flex
vertical
v-for="(item, index) in examplesList"
:key="index"
justify="center"
:size="12"
class="examples">
<p class="text-(14px [--chat-text-color]) font-bold">{{ item.title }}</p>
<component :is="item.content" />
</n-flex>
</n-flex>
</n-flex>
</n-flex>
</template>
<script setup lang="tsx">
import { NFlex } from 'naive-ui'
type Example = {
title: string
content: JSX.Element
}[]
const avatars = [
'https://avatars.githubusercontent.com/u/20943608?s=60&v=4',
'https://avatars.githubusercontent.com/u/46394163?s=60&v=4',
'https://avatars.githubusercontent.com/u/39197136?s=60&v=4',
'https://avatars.githubusercontent.com/u/19239641?s=60&v=4'
]
const examplesList: Example = [
{
title: 'AI搜索',
content: (
<NFlex vertical size={12}>
{Array.from({ length: 3 }, (_, index) => (
<NFlex key={index} class={'examples-item'}>
<img class={'rounded-12px w-55px h-45px object-fill'} src={avatars[2]} alt="" />
<NFlex vertical justify="center" class={'text-(12px [--chat-text-color]) truncate flex-1'}>
<p class="truncate w-full">你好我是机器人小助手很高兴为你服务</p>
<p>你最近怎么样</p>
</NFlex>
<svg
style={{ filter: 'drop-shadow(0 0 0.6em #13987f)' }}
class="color-#13987f p-[10px_4px] size-26px opacity-0 absolute top-1/2 right--14px transform -translate-x-1/2 -translate-y-1/2">
<use href="#Up-GPT"></use>
</svg>
</NFlex>
))}
</NFlex>
)
},
{
title: '情感消息',
content: (
<NFlex vertical size={12}>
{Array.from({ length: 3 }, (_, index) => (
<NFlex key={index} class={'examples-item'}>
<img class={'rounded-12px w-55px h-45px object-fill'} src={avatars[1]} alt="" />
<NFlex vertical justify="center" class={'text-(12px [--chat-text-color]) truncate flex-1'}>
<p class="truncate w-full">你好我是机器人小助手很高兴为你服务</p>
<p>你最近怎么样</p>
</NFlex>
<svg
style={{ filter: 'drop-shadow(0 0 0.6em #13987f)' }}
class="color-#13987f p-[10px_4px] size-26px opacity-0 absolute top-1/2 right--14px transform -translate-x-1/2 -translate-y-1/2">
<use href="#Up-GPT"></use>
</svg>
</NFlex>
))}
</NFlex>
)
}
]
</script>
<style lang="scss">
.examples {
@apply w-300px h-fit rounded-12px p-10px box-border cursor-pointer border-(solid 1px [--line-color]) shadow-md;
}
.examples-item {
@apply relative rounded-12px p-6px box-border cursor-pointer transition-all duration-600 ease-in-out;
svg {
@apply transition-all duration-600 ease-in-out;
}
&:hover {
@apply bg-[--chat-hover-color];
svg {
@apply opacity-100;
}
}
}
</style>

View File

@ -1,35 +1,46 @@
import pkg from '~/package.json'
import { Button, Select, Slider, Switch } from './model.tsx'
import { Button, Select, Slider, Switch, Input, InputNumber } from './model.tsx'
import { NFlex } from 'naive-ui'
type ConfigItemType = 'system' | 'record' | 'identity' | 'cueWords' | 'APIAddress' | 'model' | 'clear'
type ChatConfig = {
system: {
[key in ConfigItemType]: {
title: string
description?: string
features: JSX.Element
}[]
}
/** chat 设置面板配置 */
export const content: ChatConfig = {
system: [
{
title: `当前版本:${pkg.version}`,
description: '已是最新版本',
features: Button('检查更新', 'refresh')
features: <Button title={'检查更新'} icon={'refresh'} />
},
{
title: '发送键',
features: Select([
{ label: 'Enter', value: 'Enter' },
{ label: 'Ctrl + Enter', value: 'Ctrl+Enter' }
])
features: (
<Select
content={[
{ label: 'Enter', value: 'Enter' },
{ label: 'Ctrl + Enter', value: 'Ctrl+Enter' }
]}
/>
)
},
{
title: '主题',
features: Select([
{ label: '亮色', value: 'light' },
{ label: '暗黑模式', value: 'dark' },
{ label: '跟随系统', value: 'auto' }
])
features: (
<Select
content={[
{ label: '亮色', value: 'light' },
{ label: '暗黑模式', value: 'dark' },
{ label: '跟随系统', value: 'auto' }
]}
/>
)
},
{
title: '字体大小',
@ -39,7 +50,150 @@ export const content: ChatConfig = {
{
title: '自动生成标题',
description: '根据对话内容生成合适的标题',
features: Switch()
features: <Switch active={false} />
}
],
record: [
{
title: '云端数据',
description: '还没有进行同步',
features: <Button title={'配置'} icon={'setting-config'} />
},
{
title: '本地数据',
description: '1 次对话0条消息0条提示词0个身份',
features: (
<NFlex align={'center'}>
<Button title={'导入'} icon={'Export'} />
<Button title={'导出'} icon={'Importing'} />
</NFlex>
)
}
],
identity: [
{
title: '身份启动页',
description: '新建聊天时,展示身份启动页',
features: <Switch active={true} />
},
{
title: '隐藏内置身份',
description: '在所有身份列表中隐藏内置身份',
features: <Switch active={false} />
}
],
cueWords: [
{
title: '禁用提示词自动补全',
description: '在输入框开头输入/即可触发自动补全',
features: <Switch active={false} />
},
{
title: '自定义提示词列表',
description: '内置 285 条用户定义0条',
features: <Button title={'编辑'} icon={'edit'} />
}
],
APIAddress: [
{
title: '模型服务商',
description: '切换不同的服务商',
features: (
<Select
content={[
{ label: 'openAi', value: 'openAi' },
{ label: 'Azure', value: 'Azure' },
{ label: 'Google', value: 'Google' }
]}
/>
)
},
{
title: '接口地址',
description: '除默认地址外,必须包含 http(s)://',
features: <Input value={'www.baidu.com'} />
},
{
title: 'API Key',
description: '使用自定义 OpenAI key 统过密码访问限制',
features: <Input value={'123456'} isPassword={true} />
}
],
model: [
{
title: '模型(model)',
features: (
<Select
content={[
{ label: 'gpt-3.5-turbo', value: 'gpt-3.5-turbo' },
{ label: 'gpt-4o', value: 'gpt-4o' },
{ label: 'gpt-4-32k', value: 'gpt-4-32k' },
{ label: 'gpt-4-turbo', value: 'gpt-4-turbo' }
]}
/>
)
},
{
title: '随机性(temperature)',
description: '值越大,回复越随机',
features: <Slider min={0} max={10} value={5} />
},
{
title: '核采样(top_p)',
description: '与随机性类似,但不要和随机性一起更改',
features: <Slider min={0} max={10} value={5} />
},
{
title: '单次回复限制(max_tokens)',
description: '单次交互所用的最大 Token 数',
features: <InputNumber value={4000} min={2000} max={10000} />
},
{
title: '话题新鲜度(presence_penalty)',
description: '值越大,越有可能扩展到新话题',
features: <Slider min={0} max={10} value={5} />
},
{
title: '频率惩罚度(frequency_penalty)',
description: '值越大,越有可能降低重复字词',
features: <Slider min={0} max={10} value={5} />
},
{
title: '注入系统级提示信息',
description: '强制给每次请求的消息列表开头添加一个模拟 ChatGPT 的系统提示',
features: <Switch active={false} />
},
{
title: '用户输入预处理',
description: '用户最新的一条消息会埴充到此模板',
features: <Input value={'input'} />
},
{
title: '附带历史消息数',
description: '每次请求携带的历史消息数',
features: <Slider min={0} max={10} value={5} />
},
{
title: '历史消息长度压缩阈值',
description: '当未压缩的历史消息超过该值时,将进行压缩',
features: <InputNumber value={1000} min={0} max={5000} />
},
{
title: '历史摘要',
description: '自动压缩聊天记录并作为上下文发送',
features: <Switch active={true} />
}
],
clear: [
{
title: '重置所有设置',
description: '重置所有设置项回默认值',
features: <Button title={'立即重置'} isSecondary={true} />
},
{
title: '清除所有数据',
description: '清除所有聊天、设置数据',
features: <Button title={'立即清除'} isSecondary={true} />
}
]
}

View File

@ -14,24 +14,28 @@
<div class="h-1px bg-[--line-color]"></div>
<!-- 设置的主体内容 -->
<div class="flex flex-1 shadow-inner p-20px">
<n-flex
vertical
class="w-full h-fit bg-[--bg-setting-item] border-(solid 1px [--line-color]) shadow-md rounded-8px p-10px">
<n-flex vertical justify="center" v-for="(item, index) in content.system" :key="index">
<n-flex justify="space-between" :size="0" align="center" class="p-8px">
<n-flex vertical :size="4">
<p class="text-(15px [--chat-text-color]) font-bold">{{ item.title }}</p>
<p v-if="item.description" class="text-(12px [--chat-text-color])">{{ item.description }}</p>
<n-scrollbar style="max-height: calc(100vh - 104px)">
<n-flex vertical :size="20" class="p-[20px_0] shadow-inner">
<div v-for="(key, index) in content" :key="index" class="flex flex-1 p-[0_20px]">
<n-flex
vertical
class="w-full h-fit bg-[--bg-setting-item] border-(solid 1px [--line-color]) shadow-md rounded-8px p-10px">
<n-flex vertical justify="center" v-for="(item, index) in key" :key="index">
<n-flex justify="space-between" :size="0" align="center" class="p-8px">
<n-flex vertical :size="4">
<p class="text-(15px [--chat-text-color]) font-bold">{{ item.title }}</p>
<p v-if="item.description" class="text-(12px [--chat-text-color])">{{ item.description }}</p>
</n-flex>
<component :is="item.features" />
</n-flex>
<div v-if="index !== key.length - 1" class="h-1px bg-[--line-color]"></div>
</n-flex>
<component :is="item.features" />
</n-flex>
<div v-if="index !== content.system.length - 1" class="h-1px bg-[--line-color]"></div>
</n-flex>
</div>
</n-flex>
</div>
</n-scrollbar>
</template>
<script setup lang="tsx">
import router from '@/router'
@ -53,7 +57,4 @@ const handleClose = () => {
@apply size-18px;
}
}
:deep(.n-button:not(.n-button--disabled):hover) {
color: revert;
}
</style>

View File

@ -4,6 +4,7 @@ import {
NSelect,
NSlider,
NSwitch,
NInput,
NInputNumber,
NConfigProvider,
GlobalThemeOverrides
@ -18,42 +19,65 @@ const commonTheme: GlobalThemeOverrides = {
borderDisabled: '1px solid #ccc',
borderFocus: '1px solid #ccc',
boxShadowFocus: '1px solid #ccc'
},
Button: {
textColorHover: '#red'
}
}
export const Button = (title: string, icon: string) => {
return (
<>
<NButton quaternary size={'small'}>
<NFlex justify="center" align="center" size={6}>
<svg class={'size-12px'}>
<use href={`#${icon}`}></use>
</svg>
<p class={'text-12px'}>{title}</p>
</NFlex>
export const Button = defineComponent(
(props: { title: string; icon?: string; isSecondary?: boolean }) => {
const loading = ref(false)
const handleClick = () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
}
return () => (
<NButton
loading={loading.value}
onClick={handleClick}
type={props.isSecondary ? 'error' : 'default'}
quaternary={!props.isSecondary}
secondary={props.isSecondary}
size={'small'}>
{{
icon: () =>
props.icon ? (
<svg class={'size-12px'}>
<use href={`#${props.icon}`}></use>
</svg>
) : (
void 0
),
default: () => props.title
}}
</NButton>
</>
)
}
)
},
{
props: ['title', 'icon', 'isSecondary']
}
)
export const Select = (content: any[]) => {
return (
<>
export const Select = defineComponent(
(props: { content: any[] }) => {
const v = ref(props.content[0].value)
return () => (
<NSelect
class={'w-120px rounded-8px'}
consistentMenuWidth={false}
size={'small'}
options={content}
value={content[0].value}></NSelect>
</>
)
}
v-model:value={v.value}
options={props.content}
value={props.content[0].value}></NSelect>
)
},
{
props: ['content']
}
)
export const Slider = defineComponent(
(props: { value: number; max: number; min: number }) => {
(props: { value: number; max: number; min: number; isDecimal?: boolean }) => {
const v = ref(props.value)
const formatTooltip = (value: number) => `${value}px`
return () => (
@ -66,8 +90,7 @@ export const Slider = defineComponent(
formatTooltip={formatTooltip}
v-model:value={v.value}
max={props.max}
min={props.min}
step={1}></NSlider>
min={props.min}></NSlider>
</NFlex>
)
},
@ -76,15 +99,52 @@ export const Slider = defineComponent(
}
)
export const Switch = () => {
return (
<>
<NSwitch class={'text-(12px [--chat-text-color])'} size={'small'}>
export const Switch = defineComponent(
(props: { active: boolean }) => {
const v = ref(props.active)
return () => (
<NSwitch v-model:value={v.value} class={'text-(12px [--chat-text-color])'} size={'small'}>
{{
checked: () => '开启',
unchecked: () => '关闭'
}}
</NSwitch>
</>
)
}
)
},
{
props: ['active']
}
)
export const Input = defineComponent(
(props: { value: string; isPassword?: boolean }) => {
const v = ref(props.value)
return () => (
<NConfigProvider themeOverrides={commonTheme}>
<NInput
style={{ width: '160px' }}
v-model:value={v.value}
type={props.isPassword ? 'password' : 'text'}
size={'small'}
showPasswordOn={'click'}></NInput>
</NConfigProvider>
)
},
{ props: ['value', 'isPassword'] }
)
export const InputNumber = defineComponent(
(props: { value: number; max: number; min: number }) => {
const v = ref(props.value)
return () => (
<NInputNumber
style={{ width: '120px', borderRadius: '10px', border: '1px solid #ccc' }}
min={props.min}
max={props.max}
v-model:value={v.value}
step={100}
size={'small'}></NInputNumber>
)
},
{ props: ['value'] }
)

View File

@ -4,7 +4,7 @@
<ActionBar :max-w="false" :shrink="false" />
<n-flex justify="center" class="mt-15px">
<img src="@/assets/logo/hula.png" class="w-140px h-60px" alt="" />
<img src="@/assets/logo/hula.png" class="w-140px h-60px drop-shadow-xl" alt="" />
</n-flex>
<!-- 二维码 -->