diff --git a/app/api/common.ts b/app/api/common.ts index 22e71884f..a86d68617 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.tsx b/app/components/chat.tsx index 4c1686690..bd2c913d2 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 = diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 25d0584f6..69bc35175 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,6 +30,32 @@ export function PreCode(props: { children: any }) { ); } +function _MarkDownContent(props: { content: string }) { + return ( + + {props.content} + + ); +} + +export const MarkdownContent = React.memo(_MarkDownContent); + export function Markdown( props: { content: string; @@ -38,69 +65,53 @@ export function Markdown( } & React.DOMAttributes, ) { const mdRef = useRef(null); + const renderedHeight = useRef(0); + const inView = useRef(false); 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/settings.module.scss b/app/components/settings.module.scss index 30abc36df..f257a3ca4 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 385fc323b..5d0a663fe 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/locales/cn.ts b/app/locales/cn.ts index 208752c17..45dc10a92 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -116,9 +116,12 @@ const cn = { Edit: "编辑", Modal: { Title: "提示词列表", - Add: "增加一条", + Add: "新建", Search: "搜索提示词", }, + EditModal: { + Title: "编辑提示词", + }, }, HistoryCount: { Title: "附带历史消息数", @@ -223,6 +226,14 @@ const cn = { SubTitle: "现在开始,与面具背后的灵魂思维碰撞", More: "搜索更多", }, + + UI: { + Confirm: "确认", + Cancel: "取消", + Close: "关闭", + Create: "新建", + Edit: "编辑", + }, }; export type LocaleType = typeof cn; diff --git a/app/locales/de.ts b/app/locales/de.ts index 1981944fb..048e575c5 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -121,6 +121,9 @@ const de: LocaleType = { Add: "Add One", Search: "Search Prompts", }, + EditModal: { + Title: "Edit Prompt", + }, }, HistoryCount: { Title: "Anzahl der angehängten Nachrichten", @@ -230,6 +233,14 @@ const de: LocaleType = { NotShow: "Not Show Again", ConfirmNoShow: "Confirm to disable?You can enable it in settings later.", }, + + UI: { + Confirm: "Confirm", + Cancel: "Cancel", + Close: "Close", + Create: "Create", + Edit: "Edit", + }, }; export default de; diff --git a/app/locales/en.ts b/app/locales/en.ts index d73577318..e424d9b47 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -120,6 +120,9 @@ const en: LocaleType = { Add: "Add One", Search: "Search Prompts", }, + EditModal: { + Title: "Edit Prompt", + }, }, HistoryCount: { Title: "Attached Messages Count", @@ -226,6 +229,14 @@ const en: LocaleType = { NotShow: "Not Show Again", ConfirmNoShow: "Confirm to disable?You can enable it in settings later.", }, + + UI: { + Confirm: "Confirm", + Cancel: "Cancel", + Close: "Close", + Create: "Create", + Edit: "Edit", + }, }; export default en; diff --git a/app/locales/es.ts b/app/locales/es.ts index 783cd6e95..46d18a547 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -120,6 +120,9 @@ const es: LocaleType = { Add: "Add One", Search: "Search Prompts", }, + EditModal: { + Title: "Edit Prompt", + }, }, HistoryCount: { Title: "Cantidad de mensajes adjuntos", @@ -227,6 +230,14 @@ const es: LocaleType = { NotShow: "Not Show Again", ConfirmNoShow: "Confirm to disable?You can enable it in settings later.", }, + + UI: { + Confirm: "Confirm", + Cancel: "Cancel", + Close: "Close", + Create: "Create", + Edit: "Edit", + }, }; export default es; diff --git a/app/locales/it.ts b/app/locales/it.ts index 3fcc80ec9..ee9a2c2bc 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -120,6 +120,9 @@ const it: LocaleType = { Add: "Add One", Search: "Search Prompts", }, + EditModal: { + Title: "Edit Prompt", + }, }, HistoryCount: { Title: "Conteggio dei messaggi allegati", @@ -228,6 +231,14 @@ const it: LocaleType = { NotShow: "Not Show Again", ConfirmNoShow: "Confirm to disable?You can enable it in settings later.", }, + + UI: { + Confirm: "Confirm", + Cancel: "Cancel", + Close: "Close", + Create: "Create", + Edit: "Edit", + }, }; export default it; diff --git a/app/locales/jp.ts b/app/locales/jp.ts index 4bbe05125..fb693cf5b 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -122,6 +122,9 @@ const jp: LocaleType = { Add: "新規追加", Search: "プロンプトワード検索", }, + EditModal: { + Title: "编辑提示词", + }, }, HistoryCount: { Title: "履歴メッセージ数を添付", @@ -226,6 +229,14 @@ const jp: LocaleType = { NotShow: "不再展示", ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。", }, + + UI: { + Confirm: "确认", + Cancel: "取消", + Close: "关闭", + Create: "新建", + Edit: "编辑", + }, }; export default jp; diff --git a/app/locales/tr.ts b/app/locales/tr.ts index a658fcc4f..5eb4fe3e4 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -120,6 +120,9 @@ const tr: LocaleType = { Add: "Add One", Search: "Search Prompts", }, + EditModal: { + Title: "Edit Prompt", + }, }, HistoryCount: { Title: "Ekli Mesaj Sayısı", @@ -228,6 +231,14 @@ const tr: LocaleType = { NotShow: "Not Show Again", ConfirmNoShow: "Confirm to disable?You can enable it in settings later.", }, + + UI: { + Confirm: "Confirm", + Cancel: "Cancel", + Close: "Close", + Create: "Create", + Edit: "Edit", + }, }; export default tr; diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 9a0e9eac9..de964fc3f 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -118,6 +118,9 @@ const tw: LocaleType = { Add: "新增一條", Search: "搜尋提示詞", }, + EditModal: { + Title: "编辑提示词", + }, }, HistoryCount: { Title: "附帶歷史訊息數", @@ -219,6 +222,13 @@ const tw: LocaleType = { NotShow: "不再展示", ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。", }, + UI: { + Confirm: "确认", + Cancel: "取消", + Close: "关闭", + Create: "新建", + Edit: "编辑", + }, }; export default tw; diff --git a/app/store/prompt.ts b/app/store/prompt.ts index e3a2eeddc..98d4193be 100644 --- a/app/store/prompt.ts +++ b/app/store/prompt.ts @@ -17,11 +17,12 @@ export interface PromptStore { prompts: Record; add: (prompt: Prompt) => number; + get: (id: number) => Prompt | undefined; remove: (id: number) => void; search: (text: string) => Prompt[]; + update: (id: number, updater: (prompt: Prompt) => void) => void; getUserPrompts: () => Prompt[]; - updateUserPrompts: (id: number, updater: (prompt: Prompt) => void) => void; } export const SearchService = { @@ -81,6 +82,16 @@ export const usePromptStore = create()( return prompt.id!; }, + get(id) { + const targetPrompt = get().prompts[id]; + + if (!targetPrompt) { + return SearchService.builtinPrompts.find((v) => v.id === id); + } + + return targetPrompt; + }, + remove(id) { const prompts = get().prompts; delete prompts[id]; @@ -98,7 +109,7 @@ export const usePromptStore = create()( return userPrompts; }, - updateUserPrompts(id: number, updater) { + update(id: number, updater) { const prompt = get().prompts[id] ?? { title: "", content: "", diff --git a/scripts/init-proxy.sh b/scripts/init-proxy.sh index acba064f4..32e55921a 100644 --- a/scripts/init-proxy.sh +++ b/scripts/init-proxy.sh @@ -1,5 +1,6 @@ dir="$(dirname "$0")" config=$dir/proxychains.conf host_ip=$(grep nameserver /etc/resolv.conf | sed 's/nameserver //') +echo "proxying to $host_ip" cp $dir/proxychains.template.conf $config sed -i "\$s/.*/http $host_ip 7890/" $config