From 3773c43f76df07f366fc0840f7db75a6cca8f4c0 Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Fri, 17 Nov 2023 17:10:59 +0800 Subject: [PATCH] update mask feature --- README.md | 3 +- adapter/chatgpt/struct.go | 7 +-- app/src/assets/globals.less | 2 + app/src/assets/pages/home.less | 2 +- app/src/assets/pages/mask.less | 72 +++++++++++++++++++++++ app/src/components/home/ChatInterface.tsx | 3 +- app/src/components/home/ChatWrapper.tsx | 10 ++-- app/src/conf.ts | 2 +- app/src/conversation/conversation.ts | 42 ++++++++++++- app/src/conversation/manager.ts | 43 ++++++++++++-- app/src/dialogs/MaskDialog.tsx | 68 ++++++++++++++++++++- app/src/events/mask.ts | 7 +++ app/src/i18n.ts | 12 ++++ app/src/masks/prompts.ts | 42 ++++++++++++- manager/connection.go | 1 + manager/conversation/conversation.go | 12 ++-- manager/conversation/mask.go | 13 ++++ manager/manager.go | 7 +++ 18 files changed, 317 insertions(+), 31 deletions(-) create mode 100644 app/src/assets/pages/mask.less create mode 100644 app/src/events/mask.ts create mode 100644 manager/conversation/mask.go diff --git a/README.md b/README.md index 99712f5..3a18b8a 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ ## 🔨 模型 | Models - [x] OpenAI ChatGPT (GPT-3.5, GPT-4, Instruct, DALL-E 2, DALL-E 3, Text-Davincci, ...) +- [x] Azure OpenAI - [x] Anthropic Claude (claude-2, claude-instant) - [x] Slack Claude (deprecated) - [x] Sparkdesk (v1.5, v2, v3) @@ -76,8 +77,6 @@ - [x] Tencent Hunyuan - [x] 360 GPT - [ ] RWKV -- [ ] Azure OpenAI -- [ ] Baidu Qianfan ## 📚 预览 | Screenshots ![landspace](/screenshot/landspace.png) diff --git a/adapter/chatgpt/struct.go b/adapter/chatgpt/struct.go index 8851cf5..732f431 100644 --- a/adapter/chatgpt/struct.go +++ b/adapter/chatgpt/struct.go @@ -48,18 +48,15 @@ func NewChatInstanceFromConfig(v string) *ChatInstance { func NewChatInstanceFromModel(props *InstanceProps) *ChatInstance { switch props.Model { - case globals.GPT4, globals.GPT40314, globals.GPT40613, globals.GPT3Turbo1106, globals.GPT41106Preview, + case globals.GPT4, globals.GPT40314, globals.GPT40613, globals.GPT3Turbo1106, globals.GPT432k, globals.GPT432k0613, globals.GPT432k0314: return NewChatInstanceFromConfig("gpt4") - case globals.GPT4Vision, globals.GPT4Dalle, globals.Dalle3, globals.GPT4All: + case globals.GPT41106Preview, globals.GPT4Vision, globals.GPT4Dalle, globals.Dalle3, globals.GPT4All: return NewChatInstanceFromConfig("reverse") case globals.GPT3Turbo, globals.GPT3TurboInstruct, globals.GPT3Turbo0613, globals.GPT3Turbo0301, globals.GPT3Turbo16k, globals.GPT3Turbo16k0301, globals.GPT3Turbo16k0613: - if props.Plan { - return NewChatInstanceFromConfig("subscribe") - } return NewChatInstanceFromConfig("gpt3") case globals.Dalle2: diff --git a/app/src/assets/globals.less b/app/src/assets/globals.less index 5cc0d19..74cda8c 100644 --- a/app/src/assets/globals.less +++ b/app/src/assets/globals.less @@ -5,6 +5,7 @@ @layer base { :root { --background: 71 65% 97%; + --background-hover: 56 43% 97%; --background-container: 71 65% 97%; --foreground: 222.2 84% 4.9%; @@ -58,6 +59,7 @@ .dark { --background: 0 0% 0%; + --background-hover: 0 0% 7.8%; --background-container: 0 0% 7.8%; --foreground: 210 40% 98%; diff --git a/app/src/assets/pages/home.less b/app/src/assets/pages/home.less index fdc63af..93950ff 100644 --- a/app/src/assets/pages/home.less +++ b/app/src/assets/pages/home.less @@ -678,7 +678,7 @@ .chat-box { position: relative; flex-grow: 1; - margin: 0 4px; + margin-right: 4px; } .input-box { diff --git a/app/src/assets/pages/mask.less b/app/src/assets/pages/mask.less new file mode 100644 index 0000000..f9da6dc --- /dev/null +++ b/app/src/assets/pages/mask.less @@ -0,0 +1,72 @@ +.mask-dialog { + height: max-content; + max-height: 50vh; + overflow-x: hidden; + overflow-y: auto; + scrollbar-width: thin; + padding: 1rem 0.5rem 0.5rem; +} + +.mask-wrapper { + display: flex; + flex-direction: column; +} + +.mask-list { + display: flex; + flex-direction: column; + user-select: none; + border: 1px solid hsl(var(--border)); + border-radius: var(--radius); + overflow: hidden; + margin-top: 1rem; + + &::-webkit-scrollbar { + width: 0.25rem; + } +} + +.mask-item { + display: flex; + flex-direction: row; + align-items: center; + flex-shrink: 0; + cursor: pointer; + border-bottom: 1px solid hsl(var(--border)); + padding: 1rem 0; + transition: 0.2s ease-in-out; + + .mask-avatar { + width: 2.25rem; + height: 2.25rem; + padding: 0.5rem; + margin-right: 0.75rem; + margin-left: 1rem; + border-radius: var(--radius); + border: 1px solid hsl(var(--border)); + transition: 0.2s ease-in-out; + } + + .mask-content { + display: flex; + flex-direction: column; + + .mask-name { + color: hsl(var(--text)); + margin-right: 0.5rem; + } + + .mask-info { + font-size: 12px; + color: hsl(var(--text-secondary)); + } + } + + &:hover { + background-color: hsl(var(--background-hover)); + + .mask-avatar { + border-color: hsl(var(--border-active)); + } + } +} diff --git a/app/src/components/home/ChatInterface.tsx b/app/src/components/home/ChatInterface.tsx index 061545d..30377f0 100644 --- a/app/src/components/home/ChatInterface.tsx +++ b/app/src/components/home/ChatInterface.tsx @@ -29,7 +29,8 @@ function ChatInterface({ setTarget, setWorking }: ChatInterfaceProps) { ); useEffect(() => { - const working = messages.length > 0 && !messages[messages.length - 1].end; + const end = messages[messages.length - 1].end; + const working = messages.length > 0 && !(end === undefined ? true : end); setWorking?.(working); }, [messages]); diff --git a/app/src/components/home/ChatWrapper.tsx b/app/src/components/home/ChatWrapper.tsx index 5ca6f1a..a3628fb 100644 --- a/app/src/components/home/ChatWrapper.tsx +++ b/app/src/components/home/ChatWrapper.tsx @@ -108,10 +108,12 @@ function ChatWrapper() { }); } - window.addEventListener("load", () => { - const el = document.getElementById("input"); - if (el) el.focus(); - }); + useEffect(() => { + window.addEventListener("load", () => { + const el = document.getElementById("input"); + if (el) el.focus(); + }); + }, []); useEffect(() => { if (!init) return; diff --git a/app/src/conf.ts b/app/src/conf.ts index da7d37b..6f0f65e 100644 --- a/app/src/conf.ts +++ b/app/src/conf.ts @@ -8,7 +8,7 @@ import { } from "@/utils/env.ts"; import { getMemory } from "@/utils/memory.ts"; -export const version = "3.6.26"; +export const version = "3.6.27"; export const dev: boolean = getDev(); export const deploy: boolean = true; export let rest_api: string = getRestApi(deploy); diff --git a/app/src/conversation/conversation.ts b/app/src/conversation/conversation.ts index 5307a01..0b8b5f1 100644 --- a/app/src/conversation/conversation.ts +++ b/app/src/conversation/conversation.ts @@ -5,6 +5,7 @@ import { connectionEvent } from "@/events/connection.ts"; import { AppDispatch } from "@/store"; import { setMessages } from "@/store/chat.ts"; import { modelEvent } from "@/events/model.ts"; +import {Mask} from "@/masks/types.ts"; type ConversationCallback = (idx: number, message: Message[]) => boolean; @@ -16,6 +17,7 @@ export class Conversation { public model: string; public data: Message[]; public end: boolean; + public mask: Mask | null; public constructor(id: number, callback?: ConversationCallback) { if (callback) this.setCallback(callback); @@ -24,6 +26,7 @@ export class Conversation { this.id = id; this.model = ""; this.end = true; + this.mask = null; this.connection = new Connection(this.id); if (id === -1 && this.idx === -1) { @@ -33,7 +36,7 @@ export class Conversation { ); this.load(data); - this.sendEvent("share", refer); + this.sendShareEvent(refer); }); } @@ -47,7 +50,7 @@ export class Conversation { case "stop": this.end = true; this.data[this.data.length - 1].end = true; - this.sendEvent("stop"); + this.sendStopEvent(); this.triggerCallback(); break; @@ -55,7 +58,7 @@ export class Conversation { this.end = false; delete this.data[this.data.length - 1]; this.connection?.setCallback(this.useMessage()); - this.sendEvent("restart"); + this.sendRestartEvent(); break; default: @@ -75,6 +78,33 @@ export class Conversation { }); } + public sendStopEvent() { + this.sendEvent("stop"); + } + + public sendRestartEvent() { + this.sendEvent("restart"); + } + + public sendMaskEvent(mask: Mask) { + this.sendEvent("mask", JSON.stringify(mask.context)); + } + + public sendShareEvent(refer: string) { + this.sendEvent("share", refer); + } + + public preflightMask(mask: Mask) { + this.mask = mask; + } + + public presentMask() { + if (this.mask) { + this.sendMaskEvent(this.mask); + this.mask = null; + } + } + public setId(id: number): void { this.id = id; } @@ -122,6 +152,10 @@ export class Conversation { return this.model; } + public isEmpty(): boolean { + return this.getLength() === 0; + } + public toggle(dispatch: AppDispatch): void { dispatch(setMessages(this.copyMessages())); modelEvent.emit(this.getModel()); @@ -198,6 +232,7 @@ export class Conversation { public sendMessage(t: any, props: ChatProps): boolean { if (!this.end) return false; + this.presentMask(); this.addMessage({ content: props.message, role: "user", @@ -211,6 +246,7 @@ export class Conversation { public sendMessageWithRaise(t: any, id: number, props: ChatProps): boolean { if (!this.end) return false; + this.presentMask(); this.addMessage({ content: props.message, role: "user", diff --git a/app/src/conversation/manager.ts b/app/src/conversation/manager.ts index 7a97370..39ddafb 100644 --- a/app/src/conversation/manager.ts +++ b/app/src/conversation/manager.ts @@ -11,6 +11,8 @@ import { useShared } from "@/utils/hook.ts"; import { ChatProps } from "@/conversation/connection.ts"; import { AppDispatch } from "@/store"; import { sharingEvent } from "@/events/sharing.ts"; +import {maskEvent} from "@/events/mask.ts"; +import {Mask} from "@/masks/types.ts"; export class Manager { conversations: Record; @@ -22,15 +24,15 @@ export class Manager { this.conversations[-1] = this.createConversation(-1); this.current = -1; + const _this = this; sharingEvent.addEventListener(async (data) => { console.debug(`[manager] accept sharing event (refer: ${data.refer})`); + await _this.newPage(); + }); - const interval = setInterval(() => { - if (this.dispatch) { - this.toggle(this.dispatch, -1); - clearInterval(interval); - } - }, 100); + maskEvent.addEventListener(async (mask) => { + console.debug(`[manager] accept mask event (name: ${mask.name})`); + await _this.newMaskPage(mask); }); } @@ -38,6 +40,30 @@ export class Manager { this.dispatch = dispatch; } + public async newPage(): Promise { + const interval = setInterval(() => { + if (this.dispatch) { + this.toggle(this.dispatch, -1); + clearInterval(interval); + } + }, 100); + } + + public async newMaskPage(mask: Mask): Promise { + const interval = setInterval(() => { + if (this.dispatch) { + this.toggle(this.dispatch, -1); + + const instance = this.get(-1); + if (!instance) return; + + instance.load(mask.context); + instance.preflightMask(mask); + clearInterval(interval); + } + }, 100); + } + public callback(idx: number, message: Message[]): boolean { console.debug( `[manager] conversation receive message (id: ${idx}, length: ${message.length})`, @@ -75,6 +101,11 @@ export class Manager { instance.setModel(res.model); } + public async load(id: number, data: Message[]): Promise { + if (!this.conversations[id]) await this.add(id); + this.conversations[id].load(data); + } + public async toggle(dispatch: AppDispatch, id: number): Promise { if (!this.conversations[id]) await this.add(id); diff --git a/app/src/dialogs/MaskDialog.tsx b/app/src/dialogs/MaskDialog.tsx index 8d77640..7985b17 100644 --- a/app/src/dialogs/MaskDialog.tsx +++ b/app/src/dialogs/MaskDialog.tsx @@ -1,3 +1,4 @@ +import "@/assets/pages/mask.less"; import { Dialog, DialogContent, @@ -7,8 +8,71 @@ import { } from "@/components/ui/dialog.tsx"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; -import { selectMask, setMask } from "@/store/chat.ts"; +import {closeMask, selectMask, setMask} from "@/store/chat.ts"; +import {MASKS} from "@/masks/prompts.ts"; +import {Mask} from "@/masks/types.ts"; +import {Input} from "@/components/ui/input.tsx"; +import {useMemo, useState} from "react"; +import {splitList} from "@/utils/base.ts"; +import {maskEvent} from "@/events/mask.ts"; +function getEmojiSource(emoji: string): string { + return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/apple/64/${emoji}.png`; +} + +function MaskItem({ mask }: { mask: Mask }) { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + return ( +
{ + e.preventDefault(); + + maskEvent.emit(mask); + dispatch(closeMask()); + }}> + {``} +
+
{mask.name}
+
+ {t('mask.context', { length: mask.context.length })} +
+
+
+ ) +} + +function MaskSelector() { + const { t } = useTranslation(); + const [search, setSearch] = useState(""); + const arr = useMemo(() => { + if (search.trim().length === 0) return MASKS; + + const raw = splitList(search.toLowerCase(), [" ", ",", ";", "-"]); + return MASKS.filter((mask) => { + return raw.every((keyword) => ( + mask.name.toLowerCase().includes(keyword.toLowerCase()) + )); + }); + }, [search]); + + return ( +
+ setSearch(e.target.value)} placeholder={t("mask.search")} /> +
+ { + arr.length > 0 ? arr.map((mask, index) => ( + + )) : ( +

+ {t('conversation.empty')} +

+ ) + } +
+
+ ) +} function MaskDialog() { const { t } = useTranslation(); const dispatch = useDispatch(); @@ -21,7 +85,7 @@ function MaskDialog() { {t("mask.title")}
-
+
diff --git a/app/src/events/mask.ts b/app/src/events/mask.ts new file mode 100644 index 0000000..cfbe674 --- /dev/null +++ b/app/src/events/mask.ts @@ -0,0 +1,7 @@ +import {Mask} from "@/masks/types.ts"; +import {EventCommitter} from "@/events/struct.ts"; + +export const maskEvent = new EventCommitter({ + name: "mask", + destroyedAfterTrigger: true, +}); diff --git a/app/src/i18n.ts b/app/src/i18n.ts index 8516ca6..8e86df4 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -321,6 +321,8 @@ const resources = { }, mask: { title: "预设设置", + search: "搜索预设名称", + context: "包含 {{length}} 条上下文", }, }, }, @@ -654,6 +656,11 @@ const resources = { "generate-result": "Generate Result", error: "Request Failed", }, + mask: { + title: "Mask Settings", + search: "Search Mask Name", + context: "Contains {{length}} context", + }, }, }, ru: { @@ -989,6 +996,11 @@ const resources = { "generate-result": "Результат", error: "Ошибка запроса", }, + mask: { + title: "Настройки маски", + search: "Поиск по имени маски", + context: "Содержит {{length}} контекст", + }, }, }, }; diff --git a/app/src/masks/prompts.ts b/app/src/masks/prompts.ts index 5b1e16a..7134ddc 100644 --- a/app/src/masks/prompts.ts +++ b/app/src/masks/prompts.ts @@ -97,6 +97,46 @@ export const EN_MASKS: Mask[] = [ ]; export const CN_MASKS: Mask[] = [ + { // created by @ProgramZmh + avatar: "1f9d0", + name: "数学家", + context: [ + { + role: "system", + content: + "数学家擅长数学领域的各种知识。数学家的回答应该是严谨的数学语言,包括数学公式和推理过程。" + + "公式使用 $$ f $$ 包裹,推理过程使用 > 开头。" + + "推理过程中步骤之间使用空行分隔,以1. 2. 3. ...开头。" + }, + { + role: "user", + content: + "我想让你担任数学家。你需要用严谨的数学语言回答。你的回答应该包括数学公式和推理过程。", + }, + ], + lang: "cn", + builtin: true, + }, + { // created by @ProgramZmh + avatar: "1f468-200d-1f393", + name: "经济学家", + context: [ + { + role: "system", + content: + "经济学家精通各种经济学知识和理论。" + + "经济学家的回答应当以严谨的语言和深入的经济分析为主,包括宏观经济和微观经济的理论、财政政策、货币政策等等。" + + "他们必须能够合理使用经济模型和数据分析,解释和预测经济现象及其影响。" + }, + { + role: "user", + content: + "我想让你担任经济学家。你需要用严谨的语言和深入的经济分析回答。你的回答应该包括宏观经济和微观经济的理论、财政政策、货币政策等等。", + }, + ], + lang: "cn", + builtin: true, + }, { avatar: "1f5bc-fe0f", name: "以文搜图", @@ -317,7 +357,7 @@ export const CN_MASKS: Mask[] = [ }, { avatar: "1f513", - name: "越狱模式 [Jailbreak]", + name: "开发者模式", context: [ { role: "user", diff --git a/manager/connection.go b/manager/connection.go index 04f6221..e710cdf 100644 --- a/manager/connection.go +++ b/manager/connection.go @@ -15,6 +15,7 @@ const ( StopType = "stop" RestartType = "restart" ShareType = "share" + MaskType = "mask" ) type Stack chan *conversation.FormMessage diff --git a/manager/conversation/conversation.go b/manager/conversation/conversation.go index a1d97c7..bdf7212 100644 --- a/manager/conversation/conversation.go +++ b/manager/conversation/conversation.go @@ -9,6 +9,8 @@ import ( "strings" ) +const defaultConversationName = "new chat" + type Conversation struct { Auth bool `json:"auth"` UserID int64 `json:"user_id"` @@ -34,7 +36,7 @@ func NewAnonymousConversation() *Conversation { Auth: false, UserID: -1, Id: -1, - Name: "anonymous", + Name: defaultConversationName, Message: []globals.Message{}, Model: globals.GPT3Turbo, IgnoreContext: false, @@ -46,7 +48,7 @@ func NewConversation(db *sql.DB, id int64) *Conversation { Auth: true, UserID: id, Id: GetConversationLengthByUserID(db, id) + 1, - Name: "new chat", + Name: defaultConversationName, Message: []globals.Message{}, Model: globals.GPT3Turbo, } @@ -111,7 +113,7 @@ func (c *Conversation) GetName() string { } func (c *Conversation) SetName(db *sql.DB, name string) { - c.Name = name + c.Name = utils.Extract(name, 50, "...") c.SaveConversation(db) } @@ -239,12 +241,12 @@ func (c *Conversation) AddMessageFromForm(form *FormMessage) error { } func (c *Conversation) HandleMessage(db *sql.DB, form *FormMessage) bool { - head := len(c.Message) == 0 + head := len(c.Message) == 0 || c.Name == defaultConversationName if err := c.AddMessageFromForm(form); err != nil { return false } if head { - c.SetName(db, utils.Extract(form.Message, 50, "...")) + c.SetName(db, form.Message) } c.SaveConversation(db) return true diff --git a/manager/conversation/mask.go b/manager/conversation/mask.go new file mode 100644 index 0000000..5b7cf67 --- /dev/null +++ b/manager/conversation/mask.go @@ -0,0 +1,13 @@ +package conversation + +import ( + "chat/globals" + "chat/utils" +) + +func (c *Conversation) LoadMask(data string) { + message := utils.UnmarshalForm[[]globals.Message](data) + if message != nil && len(*message) > 0 { + c.InsertMessages(*message, 0) + } +} diff --git a/manager/manager.go b/manager/manager.go index f741f56..b819e37 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -73,10 +73,17 @@ func ChatAPI(c *gin.Context) { instance.LoadSharing(db, form.Message) case RestartType: if message := instance.RemoveLatestMessage(); message.Role != globals.Assistant { + conn.Send(globals.ChatSegmentResponse{ + Message: "Hello, How can I assist you?", + End: true, + }) return fmt.Errorf("message type error") } + response := ChatHandler(buf, user, instance) instance.SaveResponse(db, response) + case MaskType: + instance.LoadMask(form.Message) } return nil