From ebc5edcdc8623085dee00b84590496d5b0a32ba4 Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Mon, 30 Oct 2023 11:07:24 +0800 Subject: [PATCH] add settings feature --- app/package.json | 1 + app/pnpm-lock.yaml | 31 ++++++++++++ app/src/assets/home.less | 5 +- app/src/assets/settings.less | 30 ++++++++++++ app/src/components/home/ChatWrapper.tsx | 23 ++++++--- app/src/components/home/ModelSelector.tsx | 6 --- app/src/components/ui/checkbox.tsx | 28 +++++++++++ app/src/conf.ts | 2 +- app/src/conversation/connection.ts | 1 + app/src/dialogs/Settings.tsx | 51 ++++++++++++++++++++ app/src/dialogs/index.tsx | 2 + app/src/i18n.ts | 5 ++ app/src/store/index.ts | 2 + app/src/store/settings.ts | 48 +++++++++++++++++++ manager/conversation/conversation.go | 57 ++++++++++++++++------- 15 files changed, 260 insertions(+), 32 deletions(-) create mode 100644 app/src/assets/settings.less create mode 100644 app/src/components/ui/checkbox.tsx create mode 100644 app/src/dialogs/Settings.tsx create mode 100644 app/src/store/settings.ts diff --git a/app/package.json b/app/package.json index 863fc921..671cb34d 100644 --- a/app/package.json +++ b/app/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@radix-ui/react-alert-dialog": "^1.0.4", + "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-context-menu": "^2.1.4", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 212ab163..59625a0d 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -8,6 +8,9 @@ dependencies: '@radix-ui/react-alert-dialog': specifier: ^1.0.4 version: 1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-checkbox': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-context-menu': specifier: ^2.1.4 version: 2.1.5(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) @@ -1759,6 +1762,34 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.33)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.33)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.33)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.33)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.33)(react@18.2.0) + '@types/react': 18.2.33 + '@types/react-dom': 18.2.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} peerDependencies: diff --git a/app/src/assets/home.less b/app/src/assets/home.less index 534dddbd..92b694d2 100644 --- a/app/src/assets/home.less +++ b/app/src/assets/home.less @@ -333,11 +333,14 @@ .input-box { width: 100%; - text-align: center; color: hsl(var(--text)); white-space: pre-wrap; padding: 0 2.5rem; + &.align { + text-align: center; + } + &::placeholder { color: hsl(var(--text-secondary)); opacity: 1; diff --git a/app/src/assets/settings.less b/app/src/assets/settings.less new file mode 100644 index 00000000..7dfede13 --- /dev/null +++ b/app/src/assets/settings.less @@ -0,0 +1,30 @@ +.settings-container { + display: flex; +} + +.settings-wrapper { + display: flex; + flex-direction: column; + margin: 1.5rem 0.5rem; + width: 100%; + color: hsl(var(--text)); + + .item { + display: flex; + flex-direction: row; + align-items: center; + user-select: none; + + .value { + transition: .1s; + } + } + + & > .item { + margin-bottom: 1rem; + + &:last-child { + margin-bottom: 0; + } + } +} diff --git a/app/src/components/home/ChatWrapper.tsx b/app/src/components/home/ChatWrapper.tsx index 0a7c57a9..0257335a 100644 --- a/app/src/components/home/ChatWrapper.tsx +++ b/app/src/components/home/ChatWrapper.tsx @@ -11,7 +11,6 @@ import { } from "@/store/chat.ts"; import { manager } from "@/conversation/manager.ts"; import { formatMessage } from "@/utils/processor.ts"; -import { triggerInstallApp } from "@/utils/app.ts"; import ChatInterface from "@/components/home/ChatInterface.tsx"; import { Button } from "@/components/ui/button.tsx"; import router from "@/router.tsx"; @@ -44,6 +43,7 @@ import { clearHistoryState, getQueryParam } from "@/utils/path.ts"; import { forgetMemory, popMemory, setMemory } from "@/utils/memory.ts"; import { useToast } from "@/components/ui/use-toast.ts"; import { ToastAction } from "@/components/ui/toast.tsx"; +import {alignSelector, contextSelector, openDialog} from "@/store/settings.ts"; function ChatSpace() { const [open, setOpen] = useState(false); @@ -115,6 +115,9 @@ function ChatWrapper() { const web = useSelector(selectWeb); const messages = useSelector(selectMessages); const target = useRef(null); + const context = useSelector(contextSelector); + const align = useSelector(alignSelector); + manager.setDispatch(dispatch); function clearFile() { @@ -126,10 +129,15 @@ function ChatWrapper() { auth: boolean, model: string, web: boolean, + context: boolean, ): Promise { const message: string = formatMessage(file, data); if (message.length > 0 && data.trim().length > 0) { - if (await manager.send(t, auth, { message, web, model, type: "chat" })) { + if (await manager.send(t, auth, { + message, web, model, + ignore_context: !context, + type: "chat", + })) { forgetMemory("history"); clearFile(); return true; @@ -140,7 +148,7 @@ function ChatWrapper() { async function handleSend(auth: boolean, model: string, web: boolean) { // because of the function wrapper, we need to update the selector state using props. - if (await processSend(input, auth, model, web)) { + if (await processSend(input, auth, model, web, context)) { setInput(""); } } @@ -153,7 +161,7 @@ function ChatWrapper() { useEffect(() => { if (!init) return; const query = getQueryParam("q").trim(); - if (query.length > 0) processSend(query, auth, model, web).then(); + if (query.length > 0) processSend(query, auth, model, web, context).then(); clearHistoryState(); }, [init]); @@ -215,7 +223,7 @@ function ChatWrapper() { )} ) => { @@ -259,7 +267,10 @@ function ChatWrapper() {
{ + // triggerInstallApp(); + dispatch(openDialog()); + }} xmlns="http://www.w3.org/2000/svg" width="24" height="24" diff --git a/app/src/components/home/ModelSelector.tsx b/app/src/components/home/ModelSelector.tsx index 0cd9d306..da1a0306 100644 --- a/app/src/components/home/ModelSelector.tsx +++ b/app/src/components/home/ModelSelector.tsx @@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { selectAuthenticated } from "@/store/auth.ts"; import { useToast } from "@/components/ui/use-toast.ts"; -import { useEffect } from "react"; import { Model } from "@/conversation/types.ts"; import { modelEvent } from "@/events/model.ts"; import { isSubscribedSelector } from "@/store/subscription.ts"; @@ -30,11 +29,6 @@ function ModelSelector(props: ModelSelectorProps) { const subscription = useSelector(isSubscribedSelector); const student = useSelector(teenagerSelector); - useEffect(() => { - if (auth && model === "gpt-3.5-turbo-0613") - dispatch(setModel("gpt-3.5-turbo-16k-0613")); - }, [auth]); - modelEvent.bind((target: string) => { if (supportModels.find((m) => m.id === target)) { if (model === target) return; diff --git a/app/src/components/ui/checkbox.tsx b/app/src/components/ui/checkbox.tsx new file mode 100644 index 00000000..91e946e7 --- /dev/null +++ b/app/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/components/ui/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/app/src/conf.ts b/app/src/conf.ts index af97adb1..13da1e45 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.0"; +export const version = "3.6.1"; export const dev: boolean = getDev(); export const deploy: boolean = true; export let rest_api: string = getRestApi(deploy); diff --git a/app/src/conversation/connection.ts b/app/src/conversation/connection.ts index 0b177dae..f21f6076 100644 --- a/app/src/conversation/connection.ts +++ b/app/src/conversation/connection.ts @@ -15,6 +15,7 @@ export type ChatProps = { message: string; model: string; web?: boolean; + ignore_context?: boolean; }; type StreamCallback = (message: StreamMessage) => void; diff --git a/app/src/dialogs/Settings.tsx b/app/src/dialogs/Settings.tsx new file mode 100644 index 00000000..79e1229d --- /dev/null +++ b/app/src/dialogs/Settings.tsx @@ -0,0 +1,51 @@ +import "@/assets/settings.less"; +import {useTranslation} from "react-i18next"; +import {useDispatch, useSelector} from "react-redux"; +import {alignSelector, contextSelector, dialogSelector, setAlign, setContext, setDialog} from "@/store/settings.ts"; +import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/components/ui/dialog.tsx"; +import {Checkbox} from "@/components/ui/checkbox.tsx"; + +function Settings() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const open = useSelector(dialogSelector); + + const align = useSelector(alignSelector); + const context = useSelector(contextSelector); + + return ( + dispatch(setDialog(open))}> + + + {t("settings.title")} + +
+
+
+
+ {t('settings.align')} +
+
+ { + dispatch(setAlign(state)); + }} /> +
+
+
+ {t('settings.context')} +
+
+ { + dispatch(setContext(state)); + }} /> +
+
+
+ + + + + ) +} + +export default Settings; diff --git a/app/src/dialogs/index.tsx b/app/src/dialogs/index.tsx index caa75ee3..3ab97120 100644 --- a/app/src/dialogs/index.tsx +++ b/app/src/dialogs/index.tsx @@ -5,6 +5,7 @@ import Package from "./Package.tsx"; import Subscription from "./Subscription.tsx"; import ShareManagement from "./ShareManagement.tsx"; import Invitation from "./Invitation.tsx"; +import Settings from "@/dialogs/Settings.tsx"; function DialogManager() { return ( @@ -16,6 +17,7 @@ function DialogManager() { + ); } diff --git a/app/src/i18n.ts b/app/src/i18n.ts index 223489fd..07aad38d 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -420,6 +420,11 @@ const resources = { contact: { title: "联系我们", }, + settings: { + title: "设置", + context: "保留上下文", + align: "聊天框居中", + } }, }, ru: { diff --git a/app/src/store/index.ts b/app/src/store/index.ts index 1fae2fbb..0e0a4228 100644 --- a/app/src/store/index.ts +++ b/app/src/store/index.ts @@ -8,6 +8,7 @@ import subscriptionReducer from "./subscription"; import apiReducer from "./api"; import sharingReducer from "./sharing"; import invitationReducer from "./invitation"; +import settingsReducer from "./settings"; const store = configureStore({ reducer: { @@ -20,6 +21,7 @@ const store = configureStore({ api: apiReducer, sharing: sharingReducer, invitation: invitationReducer, + settings: settingsReducer, }, }); diff --git a/app/src/store/settings.ts b/app/src/store/settings.ts new file mode 100644 index 00000000..231d22bd --- /dev/null +++ b/app/src/store/settings.ts @@ -0,0 +1,48 @@ +import {createSlice} from "@reduxjs/toolkit"; +import {getMemory, setMemory} from "@/utils/memory.ts"; +import {RootState} from "@/store/index.ts"; + +export const settingsSlice = createSlice({ + name: "settings", + initialState: { + dialog: false, + context: getMemory("context") !== "false", + align: getMemory("align") !== "false", + }, + reducers: { + toggleDialog: (state) => { + state.dialog = !state.dialog; + }, + setDialog: (state, action) => { + state.dialog = action.payload as boolean; + }, + openDialog: (state) => { + state.dialog = true; + }, + closeDialog: (state) => { + state.dialog = false; + }, + setContext: (state, action) => { + state.context = action.payload as boolean; + setMemory("context", String(action.payload)); + }, + setAlign: (state, action) => { + state.align = action.payload as boolean; + setMemory("align", String(action.payload)); + } + }, +}) + +export const { + toggleDialog, + setDialog, + openDialog, + closeDialog, + setContext, + setAlign, +} = settingsSlice.actions; +export default settingsSlice.reducer; + +export const dialogSelector = (state: RootState): boolean => state.settings.dialog; +export const contextSelector = (state: RootState): boolean => state.settings.context; +export const alignSelector = (state: RootState): boolean => state.settings.align; diff --git a/manager/conversation/conversation.go b/manager/conversation/conversation.go index 60556e07..b407ba01 100644 --- a/manager/conversation/conversation.go +++ b/manager/conversation/conversation.go @@ -10,31 +10,34 @@ import ( ) type Conversation struct { - Auth bool `json:"auth"` - UserID int64 `json:"user_id"` - Id int64 `json:"id"` - Name string `json:"name"` - Message []globals.Message `json:"message"` - Model string `json:"model"` - EnableWeb bool `json:"enable_web"` - Shared bool `json:"shared"` + Auth bool `json:"auth"` + UserID int64 `json:"user_id"` + Id int64 `json:"id"` + Name string `json:"name"` + Message []globals.Message `json:"message"` + Model string `json:"model"` + EnableWeb bool `json:"enable_web"` + Shared bool `json:"shared"` + IgnoreContext bool `json:"ignore_context"` } type FormMessage struct { - Type string `json:"type"` - Message string `json:"message"` - Web bool `json:"web"` - Model string `json:"model"` + Type string `json:"type"` + Message string `json:"message"` + Web bool `json:"web"` + Model string `json:"model"` + IgnoreContext bool `json:"ignore_context"` } func NewAnonymousConversation() *Conversation { return &Conversation{ - Auth: false, - UserID: -1, - Id: -1, - Name: "anonymous", - Message: []globals.Message{}, - Model: globals.GPT3Turbo, + Auth: false, + UserID: -1, + Id: -1, + Name: "anonymous", + Message: []globals.Message{}, + Model: globals.GPT3Turbo, + IgnoreContext: false, } } @@ -84,6 +87,10 @@ func (c *Conversation) IsEnableWeb() bool { return c.EnableWeb } +func (c *Conversation) IsIgnoreContext() bool { + return c.IgnoreContext +} + func (c *Conversation) SetModel(model string) { if len(model) == 0 { model = globals.GPT3Turbo @@ -95,6 +102,10 @@ func (c *Conversation) SetEnableWeb(enable bool) { c.EnableWeb = enable } +func (c *Conversation) SetIgnoreContext(ignore bool) { + c.IgnoreContext = ignore +} + func (c *Conversation) GetName() string { return c.Name } @@ -129,6 +140,14 @@ func (c *Conversation) GetMessageLength() int { } func (c *Conversation) GetMessageSegment(length int) []globals.Message { + if c.IsIgnoreContext() { + if len(c.Message) >= 1 { + // get latest message + return []globals.Message{c.Message[len(c.Message)-1]} + } + // empty + return []globals.Message{} + } if length > len(c.Message) { return c.Message } @@ -203,6 +222,7 @@ func (c *Conversation) AddMessageFromByte(data []byte) (string, error) { c.AddMessageFromUser(form.Message) c.SetModel(form.Model) c.SetEnableWeb(form.Web) + c.SetIgnoreContext(form.IgnoreContext) return form.Message, nil } @@ -214,6 +234,7 @@ func (c *Conversation) AddMessageFromForm(form *FormMessage) error { c.AddMessageFromUser(form.Message) c.SetModel(form.Model) c.SetEnableWeb(form.Web) + c.SetIgnoreContext(form.IgnoreContext) return nil }