diff --git a/app/api/common.ts b/app/api/common.ts index 22e71884..a86d6861 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -26,8 +26,11 @@ export async function requestOpenai(req: NextRequest) { headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, - ...(process.env.OPENAI_ORG_ID && { "OpenAI-Organization": process.env.OPENAI_ORG_ID }), + ...(process.env.OPENAI_ORG_ID && { + "OpenAI-Organization": process.env.OPENAI_ORG_ID, + }), }, + cache: "no-store", method: req.method, body: req.body, }); diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index 2c7b95aa..02ea086b 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -67,7 +67,10 @@ export function ChatItem(props: { )} -
+
@@ -77,14 +80,14 @@ export function ChatItem(props: { } export function ChatList(props: { narrow?: boolean }) { - const [sessions, selectedIndex, selectSession, removeSession, moveSession] = - useChatStore((state) => [ + const [sessions, selectedIndex, selectSession, moveSession] = useChatStore( + (state) => [ state.sessions, state.currentSessionIndex, state.selectSession, - state.removeSession, state.moveSession, - ]); + ], + ); const chatStore = useChatStore(); const navigate = useNavigate(); diff --git a/app/components/chat.tsx b/app/components/chat.tsx index a7808bb0..7da9b8c8 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1,5 +1,5 @@ import { useDebouncedCallback } from "use-debounce"; -import { memo, useState, useRef, useEffect, useLayoutEffect } from "react"; +import { useState, useRef, useEffect, useLayoutEffect } from "react"; import SendWhiteIcon from "../icons/send-white.svg"; import BrainIcon from "../icons/brain.svg"; @@ -64,12 +64,9 @@ import { useMaskStore, } from "../store/mask"; -const Markdown = dynamic( - async () => memo((await import("./markdown")).Markdown), - { - loading: () => , - }, -); +const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { + loading: () => , +}); function exportMessages(messages: Message[], topic: string) { const mdText = @@ -394,7 +391,7 @@ export function Chat() { const onPromptSelect = (prompt: Prompt) => { setPromptHints([]); inputRef.current?.focus(); - setUserInput(prompt.content); + setTimeout(() => setUserInput(prompt.content), 60); }; // auto grow input @@ -728,6 +725,7 @@ export function Chat() { }} fontSize={fontSize} parentRef={scrollRef} + defaultShow={i >= messages.length - 10} /> {!isUser && !message.preview && ( diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 25d0584f..49bb581d 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -9,6 +9,7 @@ import { useRef, useState, RefObject, useEffect } from "react"; import { copyToClipboard } from "../utils"; import LoadingIcon from "../icons/three-dots.svg"; +import React from "react"; export function PreCode(props: { children: any }) { const ref = useRef(null); @@ -29,78 +30,94 @@ export function PreCode(props: { children: any }) { ); } +function _MarkDownContent(props: { content: string }) { + return ( + { + const href = aProps.href || ""; + const isInternal = /^\/#/i.test(href); + const target = isInternal ? "_self" : aProps.target ?? "_blank"; + return ; + }, + }} + > + {props.content} + + ); +} + +export const MarkdownContent = React.memo(_MarkDownContent); + export function Markdown( props: { content: string; loading?: boolean; fontSize?: number; parentRef: RefObject; + defaultShow?: boolean; } & React.DOMAttributes, ) { const mdRef = useRef(null); + const renderedHeight = useRef(0); + const inView = useRef(!!props.defaultShow); const parent = props.parentRef.current; const md = mdRef.current; - const rendered = useRef(true); // disable lazy loading for bad ux - const [counter, setCounter] = useState(0); - useEffect(() => { - // to triggr rerender - setCounter(counter + 1); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.loading]); + const checkInView = () => { + if (parent && md) { + const parentBounds = parent.getBoundingClientRect(); + const twoScreenHeight = Math.max(500, parentBounds.height * 2); + const mdBounds = md.getBoundingClientRect(); + const isInRange = (x: number) => + x <= parentBounds.bottom + twoScreenHeight && + x >= parentBounds.top - twoScreenHeight; + inView.current = isInRange(mdBounds.top) || isInRange(mdBounds.bottom); + } - const inView = - rendered.current || - (() => { - if (parent && md) { - const parentBounds = parent.getBoundingClientRect(); - const mdBounds = md.getBoundingClientRect(); - const isInRange = (x: number) => - x <= parentBounds.bottom && x >= parentBounds.top; - const inView = isInRange(mdBounds.top) || isInRange(mdBounds.bottom); + if (inView.current && md) { + renderedHeight.current = Math.max( + renderedHeight.current, + md.getBoundingClientRect().height, + ); + } + }; - if (inView) { - rendered.current = true; - } - - return inView; - } - })(); - - const shouldLoading = props.loading || !inView; + checkInView(); return (
0 + ? renderedHeight.current + : "auto", + }} ref={mdRef} onContextMenu={props.onContextMenu} onDoubleClickCapture={props.onDoubleClickCapture} > - {shouldLoading ? ( - - ) : ( - - {props.content} - - )} + {inView.current && + (props.loading ? ( + + ) : ( + + ))}
); } diff --git a/app/components/mask.module.scss b/app/components/mask.module.scss index 0618cc06..d66d9886 100644 --- a/app/components/mask.module.scss +++ b/app/components/mask.module.scss @@ -1,16 +1,4 @@ @import "../styles/animation.scss"; - -@keyframes search-in { - from { - opacity: 0; - transform: translateY(5vh) scaleX(0.5); - } - to { - opacity: 1; - transform: translateY(0) scaleX(1); - } -} - .mask-page { height: 100%; display: flex; @@ -23,8 +11,9 @@ .mask-filter { width: 100%; max-width: 100%; - margin-bottom: 10px; - animation: search-in ease 0.3s; + margin-bottom: 20px; + animation: slide-in ease 0.3s; + height: 40px; display: flex; @@ -32,8 +21,6 @@ flex-grow: 1; max-width: 100%; min-width: 0; - margin-bottom: 20px; - animation: search-in ease 0.3s; } .mask-filter-lang { @@ -45,10 +32,7 @@ height: 100%; margin-left: 10px; box-sizing: border-box; - - button { - padding: 10px; - } + min-width: 80px; } } diff --git a/app/components/mask.tsx b/app/components/mask.tsx index bfaab2b7..c980c48c 100644 --- a/app/components/mask.tsx +++ b/app/components/mask.tsx @@ -291,14 +291,16 @@ export function MaskPage() { ))} -
- } - text={Locale.Mask.Page.Create} - bordered - onClick={() => maskStore.create()} - /> -
+ } + text={Locale.Mask.Page.Create} + bordered + onClick={() => { + const createdMask = maskStore.create(); + setEditingMaskId(createdMask.id); + }} + />
diff --git a/app/components/new-chat.module.scss b/app/components/new-chat.module.scss index 36f447bb..b0e472ea 100644 --- a/app/components/new-chat.module.scss +++ b/app/components/new-chat.module.scss @@ -59,10 +59,9 @@ display: flex; justify-content: center; - .search-bar { + .more { font-size: 12px; - margin-right: 10px; - width: 40vw; + margin-left: 10px; } } diff --git a/app/components/new-chat.tsx b/app/components/new-chat.tsx index 8cb4d35e..42612e0a 100644 --- a/app/components/new-chat.tsx +++ b/app/components/new-chat.tsx @@ -5,10 +5,11 @@ import { EmojiAvatar } from "./emoji"; import styles from "./new-chat.module.scss"; import LeftIcon from "../icons/left.svg"; -import AddIcon from "../icons/lightning.svg"; +import LightningIcon from "../icons/lightning.svg"; +import EyeIcon from "../icons/eye.svg"; import { useLocation, useNavigate } from "react-router-dom"; -import { createEmptyMask, Mask, useMaskStore } from "../store/mask"; +import { Mask, useMaskStore } from "../store/mask"; import Locale from "../locales"; import { useAppConfig, useChatStore } from "../store"; import { MaskAvatar } from "./mask"; @@ -148,20 +149,22 @@ export function NewChat() {
{Locale.NewChat.SubTitle}
- navigate(Path.Masks)} - /> - startChat()} - icon={} + icon={} type="primary" shadow /> + + navigate(Path.Masks)} + icon={} + bordered + shadow + />
diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss index 30abc36d..f257a3ca 100644 --- a/app/components/settings.module.scss +++ b/app/components/settings.module.scss @@ -7,6 +7,20 @@ cursor: pointer; } +.edit-prompt-modal { + display: flex; + flex-direction: column; + + .edit-prompt-title { + max-width: unset; + margin-bottom: 20px; + text-align: left; + } + .edit-prompt-content { + max-width: unset; + } +} + .user-prompt-modal { min-height: 40vh; @@ -18,47 +32,42 @@ } .user-prompt-list { - padding: 10px 0; + border: var(--border-in-light); + border-radius: 10px; .user-prompt-item { - margin-bottom: 10px; - widows: 100%; + display: flex; + justify-content: space-between; + padding: 10px; + + &:not(:last-child) { + border-bottom: var(--border-in-light); + } .user-prompt-header { - display: flex; - widows: 100%; - margin-bottom: 5px; + max-width: calc(100% - 100px); .user-prompt-title { - flex-grow: 1; - max-width: 100%; - margin-right: 5px; - padding: 5px; - font-size: 12px; - text-align: left; + font-size: 14px; + line-height: 2; + font-weight: bold; } - - .user-prompt-buttons { - display: flex; - align-items: center; - - .user-prompt-button { - height: 100%; - - &:not(:last-child) { - margin-right: 5px; - } - } + .user-prompt-content { + font-size: 12px; } } - .user-prompt-content { - width: 100%; - box-sizing: border-box; - padding: 5px; - margin-right: 10px; - font-size: 12px; - flex-grow: 1; + .user-prompt-buttons { + display: flex; + align-items: center; + + .user-prompt-button { + height: 100%; + + &:not(:last-child) { + margin-right: 5px; + } + } } } } diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 385fc323..5d0a663f 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -3,10 +3,12 @@ import { useState, useEffect, useMemo, HTMLProps, useRef } from "react"; import styles from "./settings.module.scss"; import ResetIcon from "../icons/reload.svg"; +import AddIcon from "../icons/add.svg"; import CloseIcon from "../icons/close.svg"; import CopyIcon from "../icons/copy.svg"; import ClearIcon from "../icons/clear.svg"; import EditIcon from "../icons/edit.svg"; +import EyeIcon from "../icons/eye.svg"; import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib"; import { ModelConfigList } from "./model-config"; @@ -30,6 +32,55 @@ import { InputRange } from "./input-range"; import { useNavigate } from "react-router-dom"; import { Avatar, AvatarPicker } from "./emoji"; +function EditPromptModal(props: { id: number; onClose: () => void }) { + const promptStore = usePromptStore(); + const prompt = promptStore.get(props.id); + + return prompt ? ( +
+ , + ]} + > +
+ + promptStore.update( + props.id, + (prompt) => (prompt.title = e.currentTarget.value), + ) + } + > + + promptStore.update( + props.id, + (prompt) => (prompt.content = e.currentTarget.value), + ) + } + > +
+
+
+ ) : null; +} + function UserPromptModal(props: { onClose?: () => void }) { const promptStore = usePromptStore(); const userPrompts = promptStore.getUserPrompts(); @@ -39,6 +90,8 @@ function UserPromptModal(props: { onClose?: () => void }) { const [searchPrompts, setSearchPrompts] = useState([]); const prompts = searchInput.length > 0 ? searchPrompts : allPrompts; + const [editingPromptId, setEditingPromptId] = useState(); + useEffect(() => { if (searchInput.length > 0) { const searchResult = SearchService.search(searchInput); @@ -56,8 +109,13 @@ function UserPromptModal(props: { onClose?: () => void }) { actions={[ promptStore.add({ title: "", content: "" })} - icon={} + onClick={() => + promptStore.add({ + title: "Empty Prompt", + content: "Empty Prompt Content", + }) + } + icon={} bordered text={Locale.Settings.Prompt.Modal.Add} />, @@ -76,57 +134,51 @@ function UserPromptModal(props: { onClose?: () => void }) { {prompts.map((v, _) => (
- { - if (v.isUser) { - promptStore.updateUserPrompts( - v.id!, - (prompt) => (prompt.title = e.currentTarget.value), - ); - } - }} - > - -
- {v.isUser && ( - } - bordered - className={styles["user-prompt-button"]} - onClick={() => promptStore.remove(v.id!)} - /> - )} - } - bordered - className={styles["user-prompt-button"]} - onClick={() => copyToClipboard(v.content)} - /> +
{v.title}
+
+ {v.content}
- { - if (v.isUser) { - promptStore.updateUserPrompts( - v.id!, - (prompt) => (prompt.content = e.currentTarget.value), - ); - } - }} - /> + +
+ {v.isUser && ( + } + className={styles["user-prompt-button"]} + onClick={() => promptStore.remove(v.id!)} + /> + )} + {v.isUser ? ( + } + className={styles["user-prompt-button"]} + onClick={() => setEditingPromptId(v.id)} + /> + ) : ( + } + className={styles["user-prompt-button"]} + onClick={() => setEditingPromptId(v.id)} + /> + )} + } + className={styles["user-prompt-button"]} + onClick={() => copyToClipboard(v.content)} + /> +
))}
+ + {editingPromptId !== undefined && ( + setEditingPromptId(undefined)} + /> + )}
); } diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 47a31172..77621964 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -138,7 +138,11 @@ export function SideBar(props: { className?: string }) {
} - onClick={chatStore.deleteSession} + onClick={() => { + if (confirm(Locale.Home.DeleteChat)) { + chatStore.deleteSession(chatStore.currentSessionIndex); + } + }} />
diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index 5b6ed959..c16f94a4 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -158,6 +158,7 @@ export type ToastProps = { text: string; onClick: () => void; }; + onClose?: () => void; }; export function Toast(props: ToastProps) { @@ -167,7 +168,10 @@ export function Toast(props: ToastProps) { {props.content} {props.action && (