From a9c26f9f1c401d94beb88f30fa9c13ac71f9f184 Mon Sep 17 00:00:00 2001 From: 79E <5980844@qq.com> Date: Thu, 8 Jun 2023 00:30:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E7=99=BB=E5=BD=95&=E4=BC=9A=E8=AF=9D=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E7=AD=89=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README-CN.md | 10 +- README.md | 10 +- src/components/ConfigModal/index.tsx | 2 +- src/components/FormItemCard/index.tsx | 27 +- src/components/GoodsList/index.tsx | 4 +- src/components/Layout/index.tsx | 161 +++--- src/components/Reminder/index.module.less | 3 +- src/components/Reminder/index.tsx | 108 ++-- src/components/RichEdit/index.tsx | 32 +- src/pages/404/index.tsx | 4 +- src/pages/admin/config/index.tsx | 323 +++++++++--- src/pages/admin/notification/index.tsx | 96 +--- src/pages/admin/product/index.tsx | 18 +- .../components/ChatMessage/index.module.less | 31 +- .../chat/components/ChatMessage/index.tsx | 142 ++++-- src/pages/chat/components/RoleLocal/index.tsx | 6 +- src/pages/chat/index.tsx | 103 ++-- src/pages/draw/index.module.less | 228 +++++++-- src/pages/draw/index.tsx | 461 +++++++++++++++--- src/pages/shop/index.tsx | 6 +- src/request/api.ts | 11 +- src/request/index.ts | 2 +- src/store/config/slice.ts | 12 +- src/store/prompt/slice.ts | 10 +- src/store/shop/async.ts | 6 +- src/types/admin.ts | 1 + src/types/index.ts | 16 +- 27 files changed, 1297 insertions(+), 536 deletions(-) diff --git a/README-CN.md b/README-CN.md index 4d7eabe..5529242 100644 --- a/README-CN.md +++ b/README-CN.md @@ -80,20 +80,12 @@ yarn build ## ⛺️ 环境变量 -> 本项目大多数配置项都通过环境变量来设置。 +> 如果是前后端分离模式部署项目则需要填以下配置 #### `VITE_APP_REQUEST_HOST` 请求服务端的`Host`地址。 -#### `VITE_APP_TITLE` - -Chat Web 标题名称。 - -#### `VITE_APP_LOGO` - -Chat Web Logo。 - ## 🚧 开发 > 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。 diff --git a/README.md b/README.md index 2043bca..5b1a767 100644 --- a/README.md +++ b/README.md @@ -87,20 +87,12 @@ yarn build ## ⛺️ Environment Variable -> Most configuration items in this project are set through environment variables. +> If it is a front-end and back-end separation mode deployment project, the following configuration needs to be filled in #### `VITE_APP_REQUEST_HOST` Request the `Host` address of the server. -#### `VITE_APP_TITLE` - -Chat Web title. - -#### `VITE_APP_LOGO` - -Chat Web Logo. - ## 🚧 Develop > It is strongly not recommended to develop or deploy locally. Due to technical reasons, it is difficult to configure OpenAI API proxies locally, unless you can guarantee direct connection to the OpenAI server. diff --git a/src/components/ConfigModal/index.tsx b/src/components/ConfigModal/index.tsx index 73e222a..50d2610 100644 --- a/src/components/ConfigModal/index.tsx +++ b/src/components/ConfigModal/index.tsx @@ -81,7 +81,7 @@ function ConfigModal(props: Props) { */} - + diff --git a/src/components/FormItemCard/index.tsx b/src/components/FormItemCard/index.tsx index a274f63..02c697b 100644 --- a/src/components/FormItemCard/index.tsx +++ b/src/components/FormItemCard/index.tsx @@ -1,26 +1,41 @@ import useDocumentResize from '@/hooks/useDocumentResize' import styles from './index.module.less' +import { useMemo } from 'react' type Props = { title: string - describe: string + describe?: string + direction?: 'vertical' | 'horizontal' children?: React.ReactNode } function FormItemCard(props: Props) { + + const { direction = 'horizontal' } = props; + const { width } = useDocumentResize() + const style: React.CSSProperties = useMemo(()=>{ + if(direction === 'vertical'){ + return { + flexDirection: 'column', + alignItems: 'normal' + } + } + return { + flexDirection: width < 600 ? 'column' : 'row', + alignItems: width < 600 ? 'normal' : 'center' + } + }, [direction, width]) + return (

{props.title}

- {props.describe} + {props.describe && {props.describe}}
{props.children}
diff --git a/src/components/GoodsList/index.tsx b/src/components/GoodsList/index.tsx index 70c45df..0280f72 100644 --- a/src/components/GoodsList/index.tsx +++ b/src/components/GoodsList/index.tsx @@ -1,6 +1,8 @@ import { ProductInfo } from '@/types' import styles from './index.module.less' import { useEffect, useState } from 'react' +import { QuestionCircleOutlined } from '@ant-design/icons'; +import { Popover } from 'antd'; function GoodsList(props: { list: Array; onChange: (item: ProductInfo) => void }) { const [selectItem, setSelectItem] = useState() @@ -26,7 +28,7 @@ function GoodsList(props: { list: Array; onChange: (item: ProductIn setSelectItem(item) }} > -

{ item.level === 1 ? '会员' : item.level === 2 ? '超级会员' : '超级特惠' }

+

{ item.level === 1 ? '会员' : item.level === 2 ? '超级会员' : '超级特惠' } {item?.describe && }

{item.type === 'integral' ?

{item.value}积分

:

{item.value}天

}

{(item.price / 100).toFixed(2)}

diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 3694edd..a7568b3 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -1,68 +1,109 @@ -import { MenuDataItem, ProLayout } from '@ant-design/pro-components'; -import HeaderRender from '../HeaderRender'; -import { ChatsInfo } from '@/types'; -import React from 'react'; -import { MenuProps } from 'antd'; +import { MenuDataItem, ProLayout } from '@ant-design/pro-components' +import HeaderRender from '../HeaderRender' +import { ChatsInfo } from '@/types' +import React, { useEffect } from 'react' +import { MenuProps } from 'antd' +import { configStore } from '@/store' type Props = { - menuExtraRender?: () => React.ReactNode, - route?: { - path: string, - routes: Array + menuExtraRender?: () => React.ReactNode + route?: { + path: string + routes: Array + } + menuItemRender?: ( + item: MenuDataItem & { + isUrl: boolean + onClick: () => void }, - menuItemRender?:(item: MenuDataItem & { - isUrl: boolean; - onClick: () => void; - }, defaultDom: React.ReactNode, menuProps: MenuProps | any) => React.ReactNode | undefined, - menuDataRender?: ((menuData: MenuDataItem[]) => MenuDataItem[]), - menuFooterRender?: (props?: any) => React.ReactNode, - menuProps?: MenuProps, - children?: React.ReactNode + defaultDom: React.ReactNode, + menuProps: MenuProps | any + ) => React.ReactNode | undefined + menuDataRender?: (menuData: MenuDataItem[]) => MenuDataItem[] + menuFooterRender?: (props?: any) => React.ReactNode + menuProps?: MenuProps + children?: React.ReactNode } function Layout(props: Props) { - const { menuExtraRender = () => <>, menuItemRender = ()=> undefined } = props; - return ( - <>{dom} - }} - menuFooterRender={props.menuFooterRender} - menuProps={props.menuProps} - breadcrumbRender={() => []} - > - {props.children} - - ) + const { menuExtraRender = () => <>, menuItemRender = () => undefined } = props + + const { website_logo, website_title, website_footer, website_description, website_keywords } = + configStore() + + function createMetaElement(key: string, value: string) { + const isMeta = document.querySelector(`meta[name="${key}"]`) + if (!isMeta) { + const head = document.querySelector('head') + const meta = document.createElement('meta') + meta.name = key + meta.content = value + head?.appendChild(meta) + } + } + + useEffect(() => { + createMetaElement('description', website_description) + createMetaElement('keywords', website_keywords) + }, []) + + return ( + <>{dom} + }} + menuFooterRender={(p) => { + return ( +
+ {props.menuFooterRender?.(p)} + {website_footer && ( +
+ )} +
+ ) + }} + menuProps={props.menuProps} + breadcrumbRender={() => []} + > + {props.children} + + ) } -export default Layout; \ No newline at end of file +export default Layout diff --git a/src/components/Reminder/index.module.less b/src/components/Reminder/index.module.less index d3f16a5..af5e998 100644 --- a/src/components/Reminder/index.module.less +++ b/src/components/Reminder/index.module.less @@ -17,7 +17,8 @@ &_message{ font-size: 16px; color: #26334b; - margin-top: 4px; + margin-top: 8px; + line-height: 1.5; span{ padding: 2px 8px; border: 1px solid #26334b; diff --git a/src/components/Reminder/index.tsx b/src/components/Reminder/index.tsx index 7f66e16..8c7b69b 100644 --- a/src/components/Reminder/index.tsx +++ b/src/components/Reminder/index.tsx @@ -1,54 +1,62 @@ -import styles from './index.module.less'; +import styles from './index.module.less' -function Reminder(){ +function Reminder() { + const list = [ + { + id: 'zhichangzhuli', + icon: 'https://p1.meituan.net/travelcube/ff6b82c66b420ca0867244eca69196a51213.png', + name: '职场助理', + desc: '作为手机斗地主游戏的产品经理,该如何做成国内爆款?' + }, + { + id: 'dianyingjiaoben', + icon: 'https://p1.meituan.net/travelcube/114c7d1966a4c80a961ea2b51d45f30a1264.png', + name: '电影脚本', + desc: '写一段电影脚本,讲一个北漂草根创业逆袭的故事' + }, + { + id: 'cuanxieduanwen', + icon: 'https://p0.meituan.net/travelcube/1d69e439c722baad87266e4d2f8de0f0428.png', + name: '撰写短文', + desc: '写一篇短文,用故事阐释幸福的意义' + }, + { + id: 'daimabianxie', + icon: 'https://p0.meituan.net/travelcube/755ac03833e2f9e5dca8069ad1f437ff495.png', + name: '代码编写', + desc: '使用JavaScript写一个获取随机数的函数' + } + ] - const list = [ - { - id: 'zhichangzhuli', - icon: 'https://www.imageoss.com/images/2023/04/23/Frame2x11dd9e54d8caafc4b2.png', - name: '职场助理', - desc: '作为手机斗地主游戏的产品经理,该如何做成国内爆款?' - }, - { - id: 'dianyingjiaoben', - icon: 'https://www.imageoss.com/images/2023/04/23/Frame2x12ff8d52b031b85fbe.png', - name: '电影脚本', - desc: '写一段电影脚本,讲一个北漂草根创业逆袭的故事' - }, - { - id: 'cuanxieduanwen', - icon: 'https://www.imageoss.com/images/2023/04/23/Frame2x132f6276a56cf44e81.png', - name: '撰写短文', - desc: '写一篇短文,用故事阐释幸福的意义' - },{ - id: 'daimabianxie', - icon: 'https://www.imageoss.com/images/2023/04/23/Frame2x14a0f6c48d4355c6ea.png', - name: '代码编写', - desc: '使用JavaScript写一个获取随机数的函数' - } - ] - - return ( -
-

欢迎来到 {import.meta.env.VITE_APP_TITLE}

-

与AI智能聊天,畅想无限可能!基于先进的AI引擎,让你的交流更加智能、高效、便捷!

-

Shift + Enter 换行。开头输入 / 召唤 Prompt 角色预设。

-
- { - list.map((item)=>{ - return ( -
- -

{item.name}

-

{item.desc}

-
-) - }) - } - -
-
-); + return ( +
+

+ + 欢迎来到 {document.title} +

+

+ 与AI智能聊天,畅想无限可能!基于先进的AI引擎,让你的交流更加智能、高效、便捷! +

+

+ Shift + Enter 换行。开头输入 / 召唤 Prompt + AI提示指令预设。 +

+
+ {list.map((item) => { + return ( +
+ +

{item.name}

+

{item.desc}

+
+ ) + })} +
+
+ ) } -export default Reminder; +export default Reminder diff --git a/src/components/RichEdit/index.tsx b/src/components/RichEdit/index.tsx index 581ae32..92df65b 100644 --- a/src/components/RichEdit/index.tsx +++ b/src/components/RichEdit/index.tsx @@ -2,14 +2,16 @@ import ReactQuill from 'react-quill' import 'react-quill/dist/quill.snow.css' type Props = { - value?: string, - onChange: (value: string) => void; + value?: string + defaultValue?: string + onChange: (value: string) => void } function RichEdit(props: Props) { return ( diff --git a/src/pages/404/index.tsx b/src/pages/404/index.tsx index 3ae7a45..edec3d4 100644 --- a/src/pages/404/index.tsx +++ b/src/pages/404/index.tsx @@ -3,7 +3,7 @@ import styles from './index.module.less' function Page404 (){ return (
- +

抱歉,您访问的页面不存在!

请确认链接地址是否正确后重新尝试

@@ -18,4 +18,4 @@ function Page404 (){ ) } -export default Page404; \ No newline at end of file +export default Page404; diff --git a/src/pages/admin/config/index.tsx b/src/pages/admin/config/index.tsx index ad06256..6b87e4e 100644 --- a/src/pages/admin/config/index.tsx +++ b/src/pages/admin/config/index.tsx @@ -4,14 +4,16 @@ import { ProFormGroup, ProFormList, ProFormText, + ProFormTextArea, QueryFilter } from '@ant-design/pro-components' -import { Button, Form, Space, message } from 'antd' -import { useEffect, useState } from 'react' +import { Button, Form, Space, Tabs, message } from 'antd' +import { useEffect, useRef, useState } from 'react' import styles from './index.module.less' import { getAdminConfig, putAdminConfig } from '@/request/adminApi' import { ConfigInfo } from '@/types/admin' import { CloseCircleOutlined, SmileOutlined } from '@ant-design/icons' +import RichEdit from '@/components/RichEdit' function ConfigPage() { const [configs, setConfigs] = useState>([]) @@ -29,13 +31,21 @@ function ConfigPage() { ai4_ratio: number | string }>() - const [drawUsePriceForm] = Form.useForm<{ - draw_use_price: Array<{ - size: string - integral: number - }> + const [drawPriceForm] = Form.useForm<{ + draw_price: number | string }>() + const [webSiteForm] = Form.useForm<{ + website_title: string + website_description: string + website_keywords: string + website_logo: string + website_footer: string + }>() + + const shopIntroduce = useRef() + const userIntroduce = useRef() + function getConfigValue(key: string, data: Array) { const value = data.filter((c) => c.name === key)[0] return value @@ -47,7 +57,7 @@ function ConfigPage() { const historyMessageCountInfo = getConfigValue('history_message_count', data) const ai3Ratio = getConfigValue('ai3_ratio', data) const ai4Ratio = getConfigValue('ai4_ratio', data) - const drawUsePrice = getConfigValue('draw_use_price', data) + const drawPrice = getConfigValue('draw_price', data) rewardForm.setFieldsValue({ register_reward: registerRewardInfo.value, signin_reward: signinRewardInfo.value @@ -59,33 +69,34 @@ function ConfigPage() { ai3_ratio: Number(ai3Ratio.value), ai4_ratio: Number(ai4Ratio.value) }) - if (drawUsePrice && drawUsePrice.value) { - drawUsePriceForm.setFieldsValue({ - draw_use_price: JSON.parse(drawUsePrice.value) + if (drawPrice && drawPrice.value) { + drawPriceForm.setFieldsValue({ + draw_price: JSON.parse(drawPrice.value) }) } - // else { - // const drawUsePriceInitData = { - // draw_use_price: [ - // { - // size: '256x256', - // integral: 80 - // }, - // { - // size: '512x512', - // integral: 90 - // }, - // { - // size: '1024x1024', - // integral: 100 - // } - // ] - // } - // drawUsePriceForm.setFieldsValue(drawUsePriceInitData) - // onSave({ - // draw_use_price: JSON.stringify(drawUsePriceInitData.draw_use_price) - // }) - // } + + const website_title = getConfigValue('website_title', data) + const website_description = getConfigValue('website_description', data) + const website_keywords = getConfigValue('website_keywords', data) + const website_logo = getConfigValue('website_logo', data) + const website_footer = getConfigValue('website_footer', data) + webSiteForm.setFieldsValue({ + website_title: website_title.value, + website_description: website_description.value, + website_keywords: website_keywords.value, + website_logo: website_logo.value, + website_footer: website_footer.value + }) + + const shop_introduce = getConfigValue('shop_introduce', data) + if (shop_introduce && shop_introduce.value) { + shopIntroduce.current = shop_introduce.value + } + + const user_introduce = getConfigValue('user_introduce', data) + if (user_introduce && user_introduce.value) { + userIntroduce.current = user_introduce.value + } } function onGetConfig() { @@ -114,8 +125,150 @@ function ConfigPage() { }) } - return ( -
+ function IntroduceSettings() { + return ( + +
+

商城页面公告设置

+
+ { + shopIntroduce.current = value + }} + /> +
+ +
+
+

个人中心页面公告设置

+
+ { + userIntroduce.current = value + }} + /> +
+ +
+
+ ) + } + + function WebSiteSettings() { + return ( + +
+

网站设置

+ { + onRewardFormSet(configs) + }} + > + + + + + + + + + + +
+
+ ) + } + + function RewardSettings() { + return (

奖励激励

{ putAdminConfig(values).then((res) => { @@ -166,6 +321,8 @@ function ConfigPage() {

历史记录

{ @@ -194,6 +351,8 @@ function ConfigPage() { 设置1积分等于多少Token,比如:1积分=50Token,那么单次会话消耗100Token就需要扣除2积分。

{ @@ -225,60 +384,62 @@ function ConfigPage() {

绘画积分扣除设置

-

分为三个规格 256x256 512x512 1024x1024 请分别设置, 如为设置则不扣除积分。

- { - values.draw_use_price = JSON.stringify(values.draw_use_price) as any - return onSave(values) - }} +

+ 绘画计费规则为每秒消耗多少积分,比如设置10则生成一张512x512图片耗时为2秒则扣除20积分! +

+ { onRewardFormSet(configs) }} size="large" + collapsed={false} + defaultCollapsed={false} requiredMark={false} - isKeyPressSubmit={false} - submitter={{ - searchConfig: { - submitText: '保存', - resetText: '恢复' - } - }} + defaultColsNumber={79} + searchText="保存" + resetText="恢复" > - - - - - - -
+ +
+ ) + } + + return ( +
+ + }, + { + label: '奖励设置', + key: 'RewardSettings', + children: + }, + { + label: '页面说明设置', + key: 'IntroduceSettings', + children: + }, + ]} + />
) } diff --git a/src/pages/admin/notification/index.tsx b/src/pages/admin/notification/index.tsx index 41d113e..e521a4b 100644 --- a/src/pages/admin/notification/index.tsx +++ b/src/pages/admin/notification/index.tsx @@ -38,21 +38,6 @@ function NotificationPage() { content: '' }) - function getConfigValue(key: string, data: Array) { - const value = data.filter((c) => c.name === key)[0] - return value - } - - function onGetConfigs (){ - getAdminConfig().then((res)=>{ - if(res.code) return - setConfigs(res.data) - }) - } - - useEffect(() => { - onGetConfigs() - }, []) const columns: ProColumns[] = [ { @@ -81,7 +66,7 @@ function NotificationPage() { dataIndex: 'status', width: 100, render: (_, data) => ( - {data.status ? '正常' : '异常'} + {data.status ? '上线' : '下线'} ) }, { @@ -178,44 +163,11 @@ function NotificationPage() { ] }} - headerTitle={( - - - - - )} rowKey="id" search={false} bordered /> + 上线 - 下线 + 下线 @@ -341,48 +293,6 @@ function NotificationPage() { - { - putAdminConfig({ - [edidContentModal.key]: edidContentModal.content - }).then((res) => { - if (res.code) { - message.error('保存失败') - return - } - setEdidContentModal({ - open: false, - title: '', - content: '', - key: '' - }) - message.success('保存成功') - onGetConfigs() - }) - }} - onCancel={() => { - setEdidContentModal({ - open: false, - content: '', - key: '' - }) - }} - title={edidContentModal.title} - > - { - setEdidContentModal((editInfo) => { - return { - ...editInfo, - content: value - } - }) - }} - /> -
) } diff --git a/src/pages/admin/product/index.tsx b/src/pages/admin/product/index.tsx index 0f0e55e..9e161ca 100644 --- a/src/pages/admin/product/index.tsx +++ b/src/pages/admin/product/index.tsx @@ -15,7 +15,7 @@ import { ProFormText } from '@ant-design/pro-components' import { ProTable } from '@ant-design/pro-components' -import { Button, Form, Tag, message } from 'antd' +import { Button, Form, Tag, Tooltip, message } from 'antd' import { useRef, useState } from 'react' function ProductPage() { @@ -72,6 +72,18 @@ function ProductPage() { return 暂无级别 } }, + { + title: '商品描述', + dataIndex: 'describe', + ellipsis: { + showTitle: false, + }, + render: (_, data) => ( + + {data.describe} + + ), + }, { title: '状态值', dataIndex: 'status', @@ -237,7 +249,7 @@ function ProductPage() { rules={[{ required: true, message: '请输入角标' }]} /> - + diff --git a/src/pages/chat/components/ChatMessage/index.module.less b/src/pages/chat/components/ChatMessage/index.module.less index 9191a39..b80f483 100644 --- a/src/pages/chat/components/ChatMessage/index.module.less +++ b/src/pages/chat/components/ChatMessage/index.module.less @@ -1,10 +1,17 @@ .chatMessage{ display: flex; margin-top: 12px; - &_avatar{ - width: 34px; - height: 34px; - border-radius: 4px; + &_avatarCard{ + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + img { + width: 34px; + height: 34px; + border-radius: 4px; + } + } &_content{ display: flex; @@ -20,6 +27,22 @@ max-width: calc(100% - 42px); display: inline-block; padding: 12px; + position: relative; + } + &_operate{ + position: absolute; + bottom: 2px; + // left: -20px; + // right: -20px; + cursor: pointer; + &_icon{ + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + } } .left{ margin-right: auto; diff --git a/src/pages/chat/components/ChatMessage/index.tsx b/src/pages/chat/components/ChatMessage/index.tsx index 0e40bf4..4f915e5 100644 --- a/src/pages/chat/components/ChatMessage/index.tsx +++ b/src/pages/chat/components/ChatMessage/index.tsx @@ -2,13 +2,45 @@ import React, { useEffect, useMemo, useRef } from 'react' import { copyToClipboard, joinTrim } from '@/utils' import styles from './index.module.less' import OpenAiLogo from '@/components/OpenAiLogo' -import { Space, Popconfirm, message } from 'antd' +import { Space, Popconfirm, message, Dropdown } from 'antd' import MarkdownIt from 'markdown-it' import mdKatex from '@traptitech/markdown-it-katex' import mila from 'markdown-it-link-attributes' import hljs from 'highlight.js' -import { DeleteOutlined } from '@ant-design/icons' +import { CopyOutlined, DeleteOutlined, MoreOutlined, RedoOutlined } from '@ant-design/icons' + +const dropdownItems = [ + { + icon: , + label: '复制', + key: 'copyout', + }, + { + icon: , + label: '重试', + key: 'refurbish', + }, + { + icon: , + label: '删除', + key: 'delete', + }, +] + +function screenDropdownItems(status: string, position: 'left' | 'right') { + const newList = dropdownItems.filter((item) => { + if (status !== 'error' && item.key === 'delete') { + return false + } + if (position !== 'left' && (item.key === 'redoOut' || item.key === 'delete')) { + return false + } + return true; + }); + + return [...newList] +} function ChatMessage({ position, @@ -16,7 +48,8 @@ function ChatMessage({ status, time, model, - onDelChatMessage + onDelChatMessage, + onRefurbishChatMessage }: { position: 'left' | 'right' content?: string @@ -24,10 +57,30 @@ function ChatMessage({ time: string model?: string onDelChatMessage?: () => void + onRefurbishChatMessage?: () => void }) { const copyMessageKey = 'copyMessageKey' const markdownBodyRef = useRef(null) + function onCopyOut(text: string) { + copyToClipboard(text) + .then(() => { + message.open({ + key: copyMessageKey, + type: 'success', + content: '复制成功' + }) + }) + .catch(() => { + message.open({ + key: copyMessageKey, + type: 'error', + content: '复制失败' + }) + }) + } + + function addCopyEvents() { if (markdownBodyRef.current) { const copyBtn = markdownBodyRef.current.querySelectorAll('.code-block-header__copy') @@ -35,21 +88,7 @@ function ChatMessage({ btn.addEventListener('click', () => { const code = btn.parentElement?.nextElementSibling?.textContent if (code) { - copyToClipboard(code) - .then(() => { - message.open({ - key: copyMessageKey, - type: 'success', - content: '复制成功' - }) - }) - .catch(() => { - message.open({ - key: copyMessageKey, - type: 'error', - content: '复制失败' - }) - }) + onCopyOut(code); } }) }) @@ -101,25 +140,12 @@ function ChatMessage({ function chatAvatar({ icon, style }: { icon: string; style?: React.CSSProperties }) { return ( - - - {status === 'error' && ( - { - onDelChatMessage?.() - }} - onCancel={() => { - // === 无操作 === - }} - okText="Yes" - cancelText="No" - > - - - )} - +
+ +
) } @@ -130,10 +156,13 @@ function ChatMessage({ justifyContent: position === 'right' ? 'flex-end' : 'flex-start' }} > + {/* https://u1.dl0.cn/icon/chat_gpt_3.png */} + {/* https://u1.dl0.cn/icon/chat_gpt_4.png */} + {/* https://u1.dl0.cn/icon/openailogo.svg */} {position === 'left' && chatAvatar({ style: { marginRight: 8 }, - icon: model && model.indexOf('gpt-4') !== -1 ? 'https://files.catbox.moe/x5v8wq.png' : 'https://files.catbox.moe/lnulfa.png' + icon: model && model.indexOf('gpt-4') !== -1 ? 'https://u1.dl0.cn/icon/chat_gpt_4.png' : 'https://u1.dl0.cn/icon/openailogo.svg' })}
)} + +
+ { + console.log(key) + if (key === 'delete') { + onDelChatMessage?.() + } + + if(key === 'refurbish'){ + onRefurbishChatMessage?.() + } + + if (key === 'copyout' && content) { + onCopyOut(content); + } + }, + }} + > +
+ +
+
+
{position === 'right' && diff --git a/src/pages/chat/components/RoleLocal/index.tsx b/src/pages/chat/components/RoleLocal/index.tsx index 0714758..4559278 100644 --- a/src/pages/chat/components/RoleLocal/index.tsx +++ b/src/pages/chat/components/RoleLocal/index.tsx @@ -166,7 +166,7 @@ function RoleLocal() { - title="角色信息" + title="AI提示指令信息" open={promptInfoModal.open} form={promptInfoform} onOpenChange={(visible) => { @@ -219,7 +219,7 @@ function RoleLocal() { {/* 导入数据 */} { setAddPromptJson(visible) @@ -262,7 +262,7 @@ function RoleLocal() { (null) // 对话 - async function sendChatCompletions(vaule: string) { + async function sendChatCompletions(vaule: string, refurbishOptions?: ChatGpt) { if (!token) { setLoginModal(true) return } - const parentMessageId = chats.filter((c) => c.id === selectChatId)[0].id - const userMessageId = generateUUID() + const parentMessageId = refurbishOptions?.requestOptions.parentMessageId || chats.filter((c) => c.id === selectChatId)[0].id + let userMessageId = generateUUID() const requestOptions = { prompt: vaule, parentMessageId, options: filterObjectNull({ - ...config + ...config, + ...refurbishOptions?.requestOptions.options }) } - setChatInfo(selectChatId, { - id: userMessageId, - text: vaule, - dateTime: formatTime(), - status: 'pass', - role: 'user', - requestOptions - }) - const assistantMessageId = generateUUID() - setChatInfo(selectChatId, { - id: assistantMessageId, - text: '', - dateTime: formatTime(), - status: 'loading', - role: 'assistant', - requestOptions - }) + const assistantMessageId = refurbishOptions?.id || generateUUID() + if(refurbishOptions?.requestOptions.parentMessageId && refurbishOptions?.id){ + userMessageId = '' + setChatDataInfo(selectChatId, assistantMessageId, { + status: 'loading', + role: 'assistant', + text: '', + dateTime: formatTime(), + requestOptions + }) + } else { + setChatInfo(selectChatId, { + id: userMessageId, + text: vaule, + dateTime: formatTime(), + status: 'pass', + role: 'user', + requestOptions + }) + setChatInfo(selectChatId, { + id: assistantMessageId, + text: '', + dateTime: formatTime(), + status: 'loading', + role: 'assistant', + requestOptions + }) + } + const controller = new AbortController() const signal = controller.signal setFetchController(controller) @@ -285,7 +302,7 @@ ${JSON.stringify(response, null, 4)} setRoleConfigModal({ open: true }) }} > - 角色预设 + AI提示指令
- {/* AI角色预设 */} + {/* AI提示指令预设 */} (null) + const containerTwoRef = useRef(null) + const [bottom, setBottom] = useState(0); + + const [drawConfig, setDrawConfig] = useState<{ + prompt: string, + quantity: number, + width: number, + height: number, + quality?: number, + steps?: number, + style?: string, + image?: File | string + }>({ prompt: '', - n: 1, - size: '256x256', - response_format: 'url' + quantity: 1, + width: 512, + height: 512, + quality: 7, + steps: 50, + style: '', + image: '' }) + const [showImage, setShowImage] = useState(''); + const [drawType, setDrawType] = useState('openai'); + const [gptLoading, setGptLoading] = useState(false); const [drawResultData, setDrawResultData] = useState<{ loading: boolean list: Array<{ url: string }> @@ -51,6 +146,7 @@ function DrawPage() { return { ...item, ...drawConfig, + draw_type: drawType, id: generateUUID(), dateTime: formatTime() } @@ -59,6 +155,15 @@ function DrawPage() { } const onStartDraw = async () => { + console.log(drawConfig) + if(gptLoading){ + message.warning('请等待提示词优化完毕') + return + } + if(!drawConfig.prompt){ + message.warning('请输入提示词') + return + } if (!token) { setLoginModal(true) return @@ -68,21 +173,129 @@ function DrawPage() { list: [] }) - await postImagesGenerations(drawConfig, {}, { timeout: 0 }) + await postImagesGenerations({ + ...drawConfig, + draw_type: drawType + }, {}, { timeout: 0 }) .then(handleDraw) .finally(() => { setDrawResultData((dr) => ({ ...dr, loading: false })) }) } + async function optimizePrompt() { + if (!drawConfig.prompt) { + message.warning('请输入提示词!') + return + } + const controller = new AbortController() + const signal = controller.signal + setGptLoading(true) + const p = `你需要为我生成AI绘画提示词,回答的形式是: + (image we're prompting), (7 descriptivekeywords), (time of day), (Hd degree). + 这是一段段按照上述形式的示例问答: + 问题: + 参考以上midjoruney prompt formula写1个midjourney prompt内容,用英文回复,不要括号,内容:宫崎骏风格的春天小镇 + 回答: + 英文:Miyazaki Hayao-style town,Green willow and red flowers, breeze coming, dreamy colors, fantastic elements, fairy-tale situation, warm breath, shooting in the evening, 4K ultra HD + 现在严格参考以上的示例回答形式和风格(这很重要),根据以下的内容生成提示词(直接以英文输出,需要补全):${drawConfig.prompt}` + const uuid = generateUUID() + const response = await postChatCompletions({ + prompt: p, + parentMessageId: uuid + }, { + options: { + signal + } + }) + + if (!(response instanceof Response)) { + controller.abort() + setGptLoading(false) + return + } + + const reader = response.body?.getReader?.() + let allContent = '' + while (true) { + const { done, value } = (await reader?.read()) || {} + if (done) { + controller?.abort() + break + } + // 将获取到的数据片段显示在屏幕上 + const text = new TextDecoder('utf-8').decode(value) + const texts = handleChatData(text) + for (let i = 0; i < texts.length; i++) { + const {content, segment } = texts[i] + allContent += content ? content : '' + if (segment === 'stop') { + setDrawConfig((config)=>({...config, prompt: allContent})) + controller.abort() + setGptLoading(false) + break + } + if (segment === 'start') { + setDrawConfig((config)=>({...config, prompt: allContent})) + } + if (segment === 'text') { + setDrawConfig((config)=>({...config, prompt: allContent})) + } + } + } + } + + const handleScroll = () => { + const twoClientHeight = containerTwoRef.current?.clientHeight || 0; + const oneScrollTop = containerOneRef.current?.scrollTop || 0; + if (oneScrollTop > 100) { + setBottom(-(twoClientHeight + 100)); + } else { + setBottom(0); + } + } + + useLayoutEffect(() => { + containerOneRef.current?.addEventListener('scroll', handleScroll); + return () => { + containerOneRef.current?.removeEventListener('scroll', handleScroll); + }; + }, []) + + function SegmentedLabel({ icon, title }: { + icon: string, + title: string + }) { + return ( +
+ {title} + {title} +
+ ) + } + + + const showFile = async (file: any) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + setShowImage(reader.result) + }; + }; + return (
-
+
Midjourney

AI 一下,妙笔生画

@@ -143,60 +356,188 @@ function DrawPage() {
-
+
- -

图片尺寸({drawConfig.size})

- { - setDrawConfig((c) => ({ ...c, size: e.target.value })) - }} - > - 256x256 - 512x512 - 1024x1024 - -

图片数量({drawConfig.n}张)

-
- { - setDrawConfig((c) => ({ ...c, n: e })) - }} - /> - {/* */} + options={[ + { + label: , + value: 'openai' + }, + { + label: , + value: 'stablediffusion' + }, + ]} + /> +
+
+

图片宽度:

+ { + setDrawConfig((c) => ({ ...c, height: e })) + }} + /> +
+
+

生成数量({drawConfig.quantity}张):

+ { + setDrawConfig((c) => ({ ...c, quantity: e })) + }} + /> +
+
+ { + drawType === 'stablediffusion' && ( +
+
+

优化次数({drawConfig.steps}):

+ { + setDrawConfig((c) => ({ ...c, steps: e })) + }} + /> +
+
+

图像质量({drawConfig.quality}):

+ { + setDrawConfig((c) => ({ ...c, quality: e })) + }} + /> +
+
+

图像风格:

+