diff --git a/README.md b/README.md index cba9d35f..1ca37656 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,10 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s ![More](./docs/images/more.png) +## Translation + +If you want to add a new translation, read this [document](./docs/translation.md). + ## Donation [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa) diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index fa3a1cf2..99b2d022 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -240,24 +240,39 @@ &:last-child { animation: slide-in ease 0.3s; } - - &:hover { - .chat-message-actions { - opacity: 1; - transform: translateY(0px); - max-width: 100%; - height: 40px; - } - - .chat-message-action-date { - opacity: 0.2; - } - } } .chat-message-user { display: flex; flex-direction: row-reverse; + + .chat-message-header { + flex-direction: row-reverse; + } +} + +.chat-message-header { + margin-top: 20px; + display: flex; + align-items: center; + + .chat-message-actions { + display: flex; + box-sizing: border-box; + font-size: 12px; + align-items: flex-end; + justify-content: space-between; + transition: all ease 0.3s; + transform: scale(0.9) translateY(5px); + margin: 0 10px; + opacity: 0; + pointer-events: none; + + .chat-input-actions { + display: flex; + flex-wrap: nowrap; + } + } } .chat-message-container { @@ -270,6 +285,12 @@ .chat-message-edit { opacity: 0.9; } + + .chat-message-actions { + opacity: 1; + pointer-events: all; + transform: scale(1) translateY(0); + } } } @@ -278,7 +299,6 @@ } .chat-message-avatar { - margin-top: 20px; position: relative; .chat-message-edit { @@ -318,27 +338,6 @@ border: var(--border-in-light); position: relative; transition: all ease 0.3s; - - .chat-message-actions { - display: flex; - box-sizing: border-box; - font-size: 12px; - align-items: flex-end; - justify-content: space-between; - transition: all ease 0.3s 0.15s; - transform: translateX(-5px) scale(0.9) translateY(30px); - opacity: 0; - height: 0; - max-width: 0; - position: absolute; - left: 0; - z-index: 2; - - .chat-input-actions { - display: flex; - flex-wrap: nowrap; - } - } } .chat-message-action-date { diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 13105e84..02c0dd92 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -708,49 +708,46 @@ export function Chat() { let lastUserMessageIndex: number | null = null; for (let i = 0; i < session.messages.length; i += 1) { const message = session.messages[i]; - if (message.id === messageId) { - break; - } if (message.role === "user") { lastUserMessageIndex = i; } + if (message.id === messageId) { + break; + } } return lastUserMessageIndex; }; - const deleteMessage = (userIndex: number) => { - chatStore.updateCurrentSession((session) => - session.messages.splice(userIndex, 2), + const deleteMessage = (msgId?: number) => { + chatStore.updateCurrentSession( + (session) => + (session.messages = session.messages.filter((m) => m.id !== msgId)), ); }; - const onDelete = (botMessageId: number) => { - const userIndex = findLastUserIndex(botMessageId); - if (userIndex === null) return; - deleteMessage(userIndex); + const onDelete = (msgId: number) => { + deleteMessage(msgId); }; - const onResend = (botMessageId: number) => { - // find last user input message and resend - const userIndex = findLastUserIndex(botMessageId); - if (userIndex === null) return; + const onResend = (message: ChatMessage) => { + let content = message.content; + + if (message.role === "assistant" && message.id) { + const userIndex = findLastUserIndex(message.id); + if (userIndex) { + content = session.messages.at(userIndex)?.content ?? content; + } + } setIsLoading(true); - const content = session.messages[userIndex].content; - deleteMessage(userIndex); chatStore.onUserInput(content).then(() => setIsLoading(false)); inputRef.current?.focus(); }; - const onPinMessage = (botMessage: ChatMessage) => { - if (!botMessage.id) return; - const userMessageIndex = findLastUserIndex(botMessage.id); - if (userMessageIndex === null) return; - - const userMessage = session.messages[userMessageIndex]; + const onPinMessage = (message: ChatMessage) => { chatStore.updateCurrentSession((session) => - session.mask.context.push(userMessage, botMessage), + session.mask.context.push(message), ); showToast(Locale.Chat.Actions.PinToastContent, { @@ -923,11 +920,11 @@ export function Chat() { > {messages.map((message, i) => { const isUser = message.role === "user"; + const isContext = i < context.length; const showActions = - !isUser && i > 0 && !(message.preview || message.content.length === 0) && - i >= context.length; // do not show actions for context prompts + !isContext; const showTyping = message.preview || message.streaming; const shouldShowClearContextDivider = i === clearContextIndex - 1; @@ -941,64 +938,38 @@ export function Chat() { } >
-
-
- } - onClick={async () => { - const newMessage = await showPrompt( - Locale.Chat.Actions.Edit, - message.content, - 10, - ); - chatStore.updateCurrentSession((session) => { - const m = session.messages.find( - (m) => m.id === message.id, +
+
+
+ } + onClick={async () => { + const newMessage = await showPrompt( + Locale.Chat.Actions.Edit, + message.content, + 10, ); - if (m) { - m.content = newMessage; - } - }); - }} - > + chatStore.updateCurrentSession((session) => { + const m = session.messages.find( + (m) => m.id === message.id, + ); + if (m) { + m.content = newMessage; + } + }); + }} + > +
+ {isUser ? ( + + ) : ( + + )}
- {isUser ? ( - - ) : ( - - )} -
- {showTyping && ( -
- {Locale.Chat.Typing} -
- )} -
- onRightClick(e, message)} - onDoubleClickCapture={() => { - if (!isMobileScreen) return; - setUserInput(message.content); - }} - fontSize={fontSize} - parentRef={scrollRef} - defaultShow={i >= messages.length - 10} - /> {showActions && (
-
+
{message.streaming ? ( } - onClick={() => onResend(message.id ?? i)} + onClick={() => onResend(message)} /> )}
- - {showActions && ( -
- {message.date.toLocaleString()} + {showTyping && ( +
+ {Locale.Chat.Typing}
)} +
+ onRightClick(e, message)} + onDoubleClickCapture={() => { + if (!isMobileScreen) return; + setUserInput(message.content); + }} + fontSize={fontSize} + parentRef={scrollRef} + defaultShow={i >= messages.length - 10} + /> +
+ +
+ {isContext + ? Locale.Chat.IsContext + : message.date.toLocaleString()} +
{shouldShowClearContextDivider && } diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 07e87cbe..c32014be 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -26,7 +26,7 @@ const cn = { Stop: "停止", Retry: "重试", Pin: "固定", - PinToastContent: "已将 2 条对话固定至预设提示词", + PinToastContent: "已将 1 条对话固定至预设提示词", PinToastAction: "查看", Delete: "删除", Edit: "编辑", @@ -66,6 +66,7 @@ const cn = { Reset: "清除记忆", SaveAs: "存为面具", }, + IsContext: "预设提示词", }, Export: { Title: "分享聊天记录", diff --git a/app/locales/en.ts b/app/locales/en.ts index 9373e2b1..d96b978f 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -28,7 +28,7 @@ const en: LocaleType = { Stop: "Stop", Retry: "Retry", Pin: "Pin", - PinToastContent: "Pinned 2 messages to contextual prompts", + PinToastContent: "Pinned 1 messages to contextual prompts", PinToastAction: "View", Delete: "Delete", Edit: "Edit", @@ -68,6 +68,7 @@ const en: LocaleType = { Reset: "Reset to Default", SaveAs: "Save as Mask", }, + IsContext: "Contextual Prompt", }, Export: { Title: "Export Messages", diff --git a/app/store/config.ts b/app/store/config.ts index fee009c0..cf390c74 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -139,19 +139,20 @@ export const useAppConfig = create()( name: StoreKey.Config, version: 3.4, migrate(persistedState, version) { - if (version === 3.4) return persistedState as any; - const state = persistedState as ChatConfig; - state.modelConfig.sendMemory = true; - state.modelConfig.historyMessageCount = 4; - state.modelConfig.compressMessageLengthThreshold = 1000; - state.modelConfig.frequency_penalty = 0; - state.modelConfig.top_p = 1; - state.modelConfig.template = DEFAULT_INPUT_TEMPLATE; - state.dontShowMaskSplashScreen = false; - state.hideBuiltinMasks = false; - return state; + if (version < 3.4) { + state.modelConfig.sendMemory = true; + state.modelConfig.historyMessageCount = 4; + state.modelConfig.compressMessageLengthThreshold = 1000; + state.modelConfig.frequency_penalty = 0; + state.modelConfig.top_p = 1; + state.modelConfig.template = DEFAULT_INPUT_TEMPLATE; + state.dontShowMaskSplashScreen = false; + state.hideBuiltinMasks = false; + } + + return state as any; }, }, ), diff --git a/docs/translation.md b/docs/translation.md new file mode 100644 index 00000000..ebe1d6d7 --- /dev/null +++ b/docs/translation.md @@ -0,0 +1,12 @@ +# How to add a new translation? + +Assume that we are adding a new translation for `new`. + +1. copy `app/locales/en.ts` to `app/locales/new.ts`; +2. edit `new.ts`, change `const en: LocaleType = ` to `const new: PartialLocaleType`, and `export default new;`; +3. edit `app/locales/index.ts`: +4. `import new from './new.ts'`; +5. add `new` to `ALL_LANGS`; +6. add `new: "new lang"` to `ALL_LANG_OPTIONS`; +7. translate the strings in `new.ts`; +8. submit a pull request, and the author will merge it. diff --git a/package.json b/package.json index cec288f4..20b76a44 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "fuse.js": "^6.6.2", "html-to-image": "^1.11.11", "mermaid": "^10.2.3", + "nanoid": "^4.0.2", "next": "^13.4.6", "node-fetch": "^3.3.1", "react": "^18.2.0", diff --git a/yarn.lock b/yarn.lock index 4e86fd7c..1c76bd4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4639,6 +4639,11 @@ nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +nanoid@^4.0.2: + version "4.0.2" + resolved "https://registry.npmmirror.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e" + integrity sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"