Merge pull request #2185 from Yidadaa/bugfix-0628

feat: many ux improvments
This commit is contained in:
Yifei Zhang 2023-06-29 01:32:57 +08:00 committed by GitHub
commit 7748a980f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 391 additions and 166 deletions

View File

@ -1,4 +1,8 @@
import { OpenaiPath, REQUEST_TIMEOUT_MS } from "@/app/constant"; import {
DEFAULT_API_HOST,
OpenaiPath,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api"; import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api";
@ -12,6 +16,9 @@ import { prettyObject } from "@/app/utils/format";
export class ChatGPTApi implements LLMApi { export class ChatGPTApi implements LLMApi {
path(path: string): string { path(path: string): string {
let openaiUrl = useAccessStore.getState().openaiUrl; let openaiUrl = useAccessStore.getState().openaiUrl;
if (openaiUrl.length === 0) {
openaiUrl = DEFAULT_API_HOST;
}
if (openaiUrl.endsWith("/")) { if (openaiUrl.endsWith("/")) {
openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1); openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1);
} }

View File

@ -27,6 +27,26 @@
fill: white !important; fill: white !important;
} }
} }
&.danger {
color: rgba($color: red, $alpha: 0.8);
border-color: rgba($color: red, $alpha: 0.5);
background-color: rgba($color: red, $alpha: 0.05);
&:hover {
border-color: red;
background-color: rgba($color: red, $alpha: 0.1);
}
path {
fill: red !important;
}
}
&:hover,
&:focus {
border-color: var(--primary);
}
} }
.shadow { .shadow {
@ -37,10 +57,6 @@
border: var(--border-in-light); border: var(--border-in-light);
} }
.icon-button:hover {
border-color: var(--primary);
}
.icon-button-icon { .icon-button-icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
@ -56,9 +72,12 @@
} }
.icon-button-text { .icon-button-text {
margin-left: 5px;
font-size: 12px; font-size: 12px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
&:not(:first-child) {
margin-left: 5px;
}
} }

View File

@ -2,16 +2,20 @@ import * as React from "react";
import styles from "./button.module.scss"; import styles from "./button.module.scss";
export type ButtonType = "primary" | "danger" | null;
export function IconButton(props: { export function IconButton(props: {
onClick?: () => void; onClick?: () => void;
icon?: JSX.Element; icon?: JSX.Element;
type?: "primary" | "danger"; type?: ButtonType;
text?: string; text?: string;
bordered?: boolean; bordered?: boolean;
shadow?: boolean; shadow?: boolean;
className?: string; className?: string;
title?: string; title?: string;
disabled?: boolean; disabled?: boolean;
tabIndex?: number;
autoFocus?: boolean;
}) { }) {
return ( return (
<button <button
@ -25,6 +29,8 @@ export function IconButton(props: {
title={props.title} title={props.title}
disabled={props.disabled} disabled={props.disabled}
role="button" role="button"
tabIndex={props.tabIndex}
autoFocus={props.autoFocus}
> >
{props.icon && ( {props.icon && (
<div <div

View File

@ -17,6 +17,7 @@ import { Path } from "../constant";
import { MaskAvatar } from "./mask"; import { MaskAvatar } from "./mask";
import { Mask } from "../store/mask"; import { Mask } from "../store/mask";
import { useRef, useEffect } from "react"; import { useRef, useEffect } from "react";
import { showConfirm } from "./ui-lib";
export function ChatItem(props: { export function ChatItem(props: {
onClick?: () => void; onClick?: () => void;
@ -139,8 +140,11 @@ export function ChatList(props: { narrow?: boolean }) {
navigate(Path.Chat); navigate(Path.Chat);
selectSession(i); selectSession(i);
}} }}
onDelete={() => { onDelete={async () => {
if (!props.narrow || confirm(Locale.Home.DeleteChat)) { if (
!props.narrow ||
(await showConfirm(Locale.Home.DeleteChat))
) {
chatStore.deleteSession(i); chatStore.deleteSession(i);
} }
}} }}

View File

@ -251,6 +251,12 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
&:hover {
.chat-message-edit {
opacity: 0.9;
}
}
} }
.chat-message-user > .chat-message-container { .chat-message-user > .chat-message-container {
@ -259,6 +265,23 @@
.chat-message-avatar { .chat-message-avatar {
margin-top: 20px; margin-top: 20px;
position: relative;
.chat-message-edit {
position: absolute;
height: 100%;
width: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all ease 0.3s;
button {
padding: 7px;
}
}
} }
.chat-message-status { .chat-message-status {

View File

@ -23,6 +23,7 @@ import BreakIcon from "../icons/break.svg";
import SettingsIcon from "../icons/chat-settings.svg"; import SettingsIcon from "../icons/chat-settings.svg";
import DeleteIcon from "../icons/clear.svg"; import DeleteIcon from "../icons/clear.svg";
import PinIcon from "../icons/pin.svg"; import PinIcon from "../icons/pin.svg";
import EditIcon from "../icons/rename.svg";
import LightIcon from "../icons/light.svg"; import LightIcon from "../icons/light.svg";
import DarkIcon from "../icons/dark.svg"; import DarkIcon from "../icons/dark.svg";
@ -61,7 +62,7 @@ import Locale from "../locales";
import { IconButton } from "./button"; import { IconButton } from "./button";
import styles from "./chat.module.scss"; import styles from "./chat.module.scss";
import { ListItem, Modal, showToast } from "./ui-lib"; import { ListItem, Modal, showConfirm, showPrompt, showToast } from "./ui-lib";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant"; import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant";
import { Avatar } from "./emoji"; import { Avatar } from "./emoji";
@ -93,8 +94,8 @@ export function SessionConfigModel(props: { onClose: () => void }) {
icon={<ResetIcon />} icon={<ResetIcon />}
bordered bordered
text={Locale.Chat.Config.Reset} text={Locale.Chat.Config.Reset}
onClick={() => { onClick={async () => {
if (confirm(Locale.Memory.ResetConfirm)) { if (await showConfirm(Locale.Memory.ResetConfirm)) {
chatStore.updateCurrentSession( chatStore.updateCurrentSession(
(session) => (session.memoryPrompt = ""), (session) => (session.memoryPrompt = ""),
); );
@ -778,10 +779,13 @@ export function Chat() {
const [showPromptModal, setShowPromptModal] = useState(false); const [showPromptModal, setShowPromptModal] = useState(false);
const renameSession = () => { const renameSession = () => {
const newTopic = prompt(Locale.Chat.Rename, session.topic); showPrompt(Locale.Chat.Rename, session.topic).then((newTopic) => {
if (newTopic && newTopic !== session.topic) { if (newTopic && newTopic !== session.topic) {
chatStore.updateCurrentSession((session) => (session.topic = newTopic!)); chatStore.updateCurrentSession(
(session) => (session.topic = newTopic!),
);
} }
});
}; };
const clientConfig = useMemo(() => getClientConfig(), []); const clientConfig = useMemo(() => getClientConfig(), []);
@ -899,6 +903,25 @@ export function Chat() {
> >
<div className={styles["chat-message-container"]}> <div className={styles["chat-message-container"]}>
<div className={styles["chat-message-avatar"]}> <div className={styles["chat-message-avatar"]}>
<div className={styles["chat-message-edit"]}>
<IconButton
icon={<EditIcon />}
onClick={async () => {
const newMessage = await showPrompt(
Locale.Chat.Actions.Edit,
message.content,
);
chatStore.updateCurrentSession((session) => {
const m = session.messages.find(
(m) => m.id === message.id,
);
if (m) {
m.content = newMessage;
}
});
}}
></IconButton>
</div>
{message.role === "user" ? ( {message.role === "user" ? (
<Avatar avatar={config.avatar} /> <Avatar avatar={config.avatar} />
) : ( ) : (

View File

@ -5,6 +5,7 @@ import ResetIcon from "../icons/reload.svg";
import { ISSUE_URL } from "../constant"; import { ISSUE_URL } from "../constant";
import Locale from "../locales"; import Locale from "../locales";
import { downloadAs } from "../utils"; import { downloadAs } from "../utils";
import { showConfirm } from "./ui-lib";
interface IErrorBoundaryState { interface IErrorBoundaryState {
hasError: boolean; hasError: boolean;
@ -57,10 +58,11 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
<IconButton <IconButton
icon={<ResetIcon />} icon={<ResetIcon />}
text="Clear All Data" text="Clear All Data"
onClick={() => onClick={async () => {
confirm(Locale.Settings.Actions.ConfirmClearAll) && if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
this.clearAndSaveData() this.clearAndSaveData();
} }
}}
bordered bordered
/> />
</div> </div>

View File

@ -15,7 +15,15 @@ import CopyIcon from "../icons/copy.svg";
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask"; import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
import { ChatMessage, ModelConfig, useAppConfig, useChatStore } from "../store"; import { ChatMessage, ModelConfig, useAppConfig, useChatStore } from "../store";
import { ROLES } from "../client/api"; import { ROLES } from "../client/api";
import { Input, List, ListItem, Modal, Popover, Select } from "./ui-lib"; import {
Input,
List,
ListItem,
Modal,
Popover,
Select,
showConfirm,
} from "./ui-lib";
import { Avatar, AvatarPicker } from "./emoji"; import { Avatar, AvatarPicker } from "./emoji";
import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales"; import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@ -125,10 +133,10 @@ export function MaskConfig(props: {
<input <input
type="checkbox" type="checkbox"
checked={props.mask.syncGlobalConfig} checked={props.mask.syncGlobalConfig}
onChange={(e) => { onChange={async (e) => {
if ( if (
e.currentTarget.checked && e.currentTarget.checked &&
confirm(Locale.Mask.Config.Sync.Confirm) (await showConfirm(Locale.Mask.Config.Sync.Confirm))
) { ) {
props.updateMask((mask) => { props.updateMask((mask) => {
mask.syncGlobalConfig = e.currentTarget.checked; mask.syncGlobalConfig = e.currentTarget.checked;
@ -439,8 +447,8 @@ export function MaskPage() {
<IconButton <IconButton
icon={<DeleteIcon />} icon={<DeleteIcon />}
text={Locale.Mask.Item.Delete} text={Locale.Mask.Item.Delete}
onClick={() => { onClick={async () => {
if (confirm(Locale.Mask.Item.DeleteConfirm)) { if (await showConfirm(Locale.Mask.Item.DeleteConfirm)) {
maskStore.delete(m.id); maskStore.delete(m.id);
} }
}} }}

View File

@ -14,6 +14,7 @@ import Locale from "../locales";
import { useAppConfig, useChatStore } from "../store"; import { useAppConfig, useChatStore } from "../store";
import { MaskAvatar } from "./mask"; import { MaskAvatar } from "./mask";
import { useCommand } from "../command"; import { useCommand } from "../command";
import { showConfirm } from "./ui-lib";
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) { function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
const xmin = Math.max(aRect.x, bRect.x); const xmin = Math.max(aRect.x, bRect.x);
@ -125,8 +126,8 @@ export function NewChat() {
{!state?.fromHome && ( {!state?.fromHome && (
<IconButton <IconButton
text={Locale.NewChat.NotShow} text={Locale.NewChat.NotShow}
onClick={() => { onClick={async () => {
if (confirm(Locale.NewChat.ConfirmNoShow)) { if (await showConfirm(Locale.NewChat.ConfirmNoShow)) {
startChat(); startChat();
config.update( config.update(
(config) => (config.dontShowMaskSplashScreen = true), (config) => (config.dontShowMaskSplashScreen = true),

View File

@ -18,6 +18,7 @@ import {
PasswordInput, PasswordInput,
Popover, Popover,
Select, Select,
showConfirm,
} from "./ui-lib"; } from "./ui-lib";
import { ModelConfigList } from "./model-config"; import { ModelConfigList } from "./model-config";
@ -199,6 +200,44 @@ function UserPromptModal(props: { onClose?: () => void }) {
); );
} }
function DangerItems() {
const chatStore = useChatStore();
const appConfig = useAppConfig();
return (
<List>
<ListItem
title={Locale.Settings.Danger.Reset.Title}
subTitle={Locale.Settings.Danger.Reset.SubTitle}
>
<IconButton
text={Locale.Settings.Danger.Reset.Action}
onClick={async () => {
if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
appConfig.reset();
}
}}
type="danger"
/>
</ListItem>
<ListItem
title={Locale.Settings.Danger.Clear.Title}
subTitle={Locale.Settings.Danger.Clear.SubTitle}
>
<IconButton
text={Locale.Settings.Danger.Clear.Action}
onClick={async () => {
if (await showConfirm(Locale.Settings.Danger.Clear.Confirm)) {
chatStore.clearAllData();
}
}}
type="danger"
/>
</ListItem>
</List>
);
}
function SyncItems() { function SyncItems() {
const syncStore = useSyncStore(); const syncStore = useSyncStore();
const webdav = syncStore.webDavConfig; const webdav = syncStore.webDavConfig;
@ -289,7 +328,6 @@ export function Settings() {
const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const config = useAppConfig(); const config = useAppConfig();
const updateConfig = config.update; const updateConfig = config.update;
const resetConfig = config.reset;
const chatStore = useChatStore(); const chatStore = useChatStore();
const updateStore = useUpdateStore(); const updateStore = useUpdateStore();
@ -374,36 +412,13 @@ export function Settings() {
</div> </div>
</div> </div>
<div className="window-actions"> <div className="window-actions">
<div className="window-action-button"> <div className="window-action-button"></div>
<IconButton <div className="window-action-button"></div>
icon={<ClearIcon />}
onClick={() => {
if (confirm(Locale.Settings.Actions.ConfirmClearAll)) {
chatStore.clearAllData();
}
}}
bordered
title={Locale.Settings.Actions.ClearAll}
/>
</div>
<div className="window-action-button">
<IconButton
icon={<ResetIcon />}
onClick={() => {
if (confirm(Locale.Settings.Actions.ConfirmResetAll)) {
resetConfig();
}
}}
bordered
title={Locale.Settings.Actions.ResetAll}
/>
</div>
<div className="window-action-button"> <div className="window-action-button">
<IconButton <IconButton
icon={<CloseIcon />} icon={<CloseIcon />}
onClick={() => navigate(Path.Home)} onClick={() => navigate(Path.Home)}
bordered bordered
title={Locale.Settings.Actions.Close}
/> />
</div> </div>
</div> </div>
@ -686,6 +701,8 @@ export function Settings() {
{shouldShowPromptModal && ( {shouldShowPromptModal && (
<UserPromptModal onClose={() => setShowPromptModal(false)} /> <UserPromptModal onClose={() => setShowPromptModal(false)} />
)} )}
<DangerItems />
</div> </div>
</ErrorBoundary> </ErrorBoundary>
); );

View File

@ -26,7 +26,7 @@ import {
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { useMobileScreen } from "../utils"; import { useMobileScreen } from "../utils";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { showToast } from "./ui-lib"; import { showConfirm, showToast } from "./ui-lib";
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, { const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => null, loading: () => null,
@ -160,8 +160,8 @@ export function SideBar(props: { className?: string }) {
<div className={styles["sidebar-action"] + " " + styles.mobile}> <div className={styles["sidebar-action"] + " " + styles.mobile}>
<IconButton <IconButton
icon={<CloseIcon />} icon={<CloseIcon />}
onClick={() => { onClick={async () => {
if (confirm(Locale.Home.DeleteChat)) { if (await showConfirm(Locale.Home.DeleteChat)) {
chatStore.deleteSession(chatStore.currentSessionIndex); chatStore.deleteSession(chatStore.currentSessionIndex);
} }
}} }}

View File

@ -228,3 +228,23 @@
pointer-events: none; pointer-events: none;
} }
} }
.modal-input {
height: 100%;
width: 100%;
border-radius: 10px;
border: var(--border-in-light);
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
background-color: var(--white);
color: var(--black);
font-family: inherit;
padding: 10px 90px 10px 14px;
resize: none;
outline: none;
box-sizing: border-box;
min-height: 68px;
&:focus {
border: 1px solid var(--primary);
}
}

View File

@ -4,6 +4,10 @@ import CloseIcon from "../icons/close.svg";
import EyeIcon from "../icons/eye.svg"; import EyeIcon from "../icons/eye.svg";
import EyeOffIcon from "../icons/eye-off.svg"; import EyeOffIcon from "../icons/eye-off.svg";
import DownIcon from "../icons/down.svg"; import DownIcon from "../icons/down.svg";
import ConfirmIcon from "../icons/confirm.svg";
import CancelIcon from "../icons/cancel.svg";
import Locale from "../locales";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import React, { HTMLProps, useEffect, useState } from "react"; import React, { HTMLProps, useEffect, useState } from "react";
@ -87,7 +91,7 @@ export function Loading() {
interface ModalProps { interface ModalProps {
title: string; title: string;
children?: JSX.Element | JSX.Element[]; children?: any;
actions?: JSX.Element[]; actions?: JSX.Element[];
onClose?: () => void; onClose?: () => void;
} }
@ -262,3 +266,128 @@ export function Select(
</div> </div>
); );
} }
export function showConfirm(content: any) {
const div = document.createElement("div");
div.className = "modal-mask";
document.body.appendChild(div);
const root = createRoot(div);
const closeModal = () => {
root.unmount();
div.remove();
};
return new Promise<boolean>((resolve) => {
root.render(
<Modal
title={Locale.UI.Confirm}
actions={[
<IconButton
key="cancel"
text={Locale.UI.Cancel}
onClick={() => {
resolve(false);
closeModal();
}}
icon={<CancelIcon />}
tabIndex={0}
bordered
shadow
></IconButton>,
<IconButton
key="confirm"
text={Locale.UI.Confirm}
type="primary"
onClick={() => {
resolve(true);
closeModal();
}}
icon={<ConfirmIcon />}
tabIndex={0}
autoFocus
bordered
shadow
></IconButton>,
]}
onClose={closeModal}
>
{content}
</Modal>,
);
});
}
function PromptInput(props: {
value: string;
onChange: (value: string) => void;
}) {
const [input, setInput] = useState(props.value);
const onInput = (value: string) => {
props.onChange(value);
setInput(value);
};
return (
<textarea
className={styles["modal-input"]}
autoFocus
value={input}
onInput={(e) => onInput(e.currentTarget.value)}
></textarea>
);
}
export function showPrompt(content: any, value = "") {
const div = document.createElement("div");
div.className = "modal-mask";
document.body.appendChild(div);
const root = createRoot(div);
const closeModal = () => {
root.unmount();
div.remove();
};
return new Promise<string>((resolve) => {
let userInput = "";
root.render(
<Modal
title={content}
actions={[
<IconButton
key="cancel"
text={Locale.UI.Cancel}
onClick={() => {
closeModal();
}}
icon={<CancelIcon />}
bordered
shadow
tabIndex={0}
></IconButton>,
<IconButton
key="confirm"
text={Locale.UI.Confirm}
type="primary"
onClick={() => {
resolve(userInput);
closeModal();
}}
icon={<ConfirmIcon />}
bordered
shadow
tabIndex={0}
></IconButton>,
]}
onClose={closeModal}
>
<PromptInput
onChange={(val) => (userInput = val)}
value={value}
></PromptInput>
</Modal>,
);
});
}

6
app/global.d.ts vendored
View File

@ -9,3 +9,9 @@ declare module "*.scss" {
} }
declare module "*.svg"; declare module "*.svg";
declare interface Window {
__TAURI__?: {
writeText(text: string): Promise<void>;
};
}

1
app/icons/cancel.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="fill:#333333; opacity:1;" d="M13.9967,8.00337c0,-0.81625 -0.1569,-1.59615 -0.4707,-2.3397c-0.30307,-0.71824 -0.73117,-1.3542 -1.2843,-1.90789c-0.55287,-0.55348 -1.18783,-0.98185 -1.9049,-1.28512c-0.74182,-0.31375 -1.51963,-0.47062 -2.33343,-0.47062c-0.81634,0 -1.59621,0.15693 -2.3396,0.47079c-0.71828,0.30325 -1.35419,0.73165 -1.90774,1.2852c-0.55355,0.55354 -0.98195,1.18945 -1.2852,1.90774c-0.31386,0.74339 -0.47079,1.52326 -0.47079,2.3396c0,0.8138 0.15687,1.59161 0.47062,2.33343c0.30327,0.71707 0.73164,1.35203 1.28512,1.9049c0.55369,0.55313 1.18965,0.98123 1.90789,1.2843c0.74355,0.3138 1.52345,0.4707 2.3397,0.4707c0.81371,0 1.59155,-0.15683 2.33353,-0.4705c0.717,-0.30313 1.35203,-0.7312 1.9051,-1.2842c0.553,-0.55307 0.98107,-1.1881 1.2842,-1.9051c0.31367,-0.74198 0.4705,-1.51982 0.4705,-2.33353zM15.33,8.00337c0,0.99387 -0.1919,1.94478 -0.5757,2.85273c-0.37067,0.87673 -0.89383,1.65297 -1.5695,2.3287c-0.67573,0.67567 -1.45197,1.19883 -2.3287,1.5695c-0.90795,0.3838 -1.85886,0.5757 -2.85273,0.5757c-0.99612,0 -1.94882,-0.19183 -2.8581,-0.5755c-0.8781,-0.3706 -1.65537,-0.89377 -2.33181,-1.5695c-0.67631,-0.6756 -1.19992,-1.45187 -1.57081,-2.3288c-0.38396,-0.90784 -0.57594,-1.85878 -0.57594,-2.85283c0,-0.99629 0.19192,-1.94903 0.57577,-2.8582c0.37081,-0.87829 0.89439,-1.6556 1.57074,-2.33195c0.67635,-0.67635 1.45367,-1.19993 2.33195,-1.57074c0.90917,-0.38385 1.86191,-0.57577 2.8582,-0.57577c0.99405,0 1.94499,0.19198 2.85283,0.57594c0.87693,0.37089 1.6532,0.8945 2.3288,1.57081c0.67573,0.67644 1.1989,1.45371 1.5695,2.33181c0.38367,0.90928 0.5755,1.86198 0.5755,2.8581z"></path><path id="路径 2" style="fill:#333333; opacity:1;" d="M5.4714,4.5286l6,6c0.03093,0.03093 0.05857,0.0646 0.0829,0.101c0.02433,0.0364 0.04487,0.07483 0.0616,0.1153c0.01673,0.0404 0.0294,0.08207 0.038,0.125c0.00853,0.04293 0.0128,0.0863 0.0128,0.1301c0,0.0438 -0.00427,0.08717 -0.0128,0.1301c-0.0086,0.04293 -0.02127,0.0846 -0.038,0.125c-0.01673,0.04047 -0.03727,0.0789 -0.0616,0.1153c-0.02433,0.0364 -0.05197,0.07007 -0.0829,0.101c-0.03093,0.03093 -0.0646,0.05857 -0.101,0.0829c-0.0364,0.02433 -0.07483,0.04487 -0.1153,0.0616c-0.0404,0.01673 -0.08207,0.0294 -0.125,0.038c-0.04293,0.00853 -0.0863,0.0128 -0.1301,0.0128c-0.0438,0 -0.08717,-0.00427 -0.1301,-0.0128c-0.04293,-0.0086 -0.0846,-0.02127 -0.125,-0.038c-0.04047,-0.01673 -0.0789,-0.03727 -0.1153,-0.0616c-0.0364,-0.02433 -0.07007,-0.05197 -0.101,-0.0829l-6,-6c-0.03095,-0.03095 -0.05859,-0.06463 -0.08291,-0.10102c-0.02432,-0.0364 -0.04486,-0.07482 -0.06161,-0.11526c-0.01675,-0.04044 -0.0294,-0.08213 -0.03794,-0.12506c-0.00854,-0.04293 -0.01281,-0.08629 -0.01281,-0.13006c0,-0.04377 0.00427,-0.08713 0.01281,-0.13006c0.00854,-0.04293 0.02119,-0.08462 0.03794,-0.12506c0.01675,-0.04045 0.03729,-0.07887 0.06161,-0.11526c0.02432,-0.0364 0.05196,-0.07007 0.08291,-0.10102c0.03095,-0.03095 0.06462,-0.05859 0.10102,-0.08291c0.03639,-0.02432 0.07481,-0.04486 0.11526,-0.06161c0.04044,-0.01675 0.08213,-0.0294 0.12506,-0.03794c0.04293,-0.00854 0.08629,-0.01281 0.13006,-0.01281c0.04377,0 0.08713,0.00427 0.13006,0.01281c0.04293,0.00854 0.08462,0.02119 0.12506,0.03794c0.04044,0.01675 0.07886,0.03729 0.11526,0.06161c0.03639,0.02432 0.07007,0.05196 0.10102,0.08291z"></path></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

1
app/icons/confirm.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="fill:#333333; opacity:1;" d="M5.99607,12.8916c-0.03633,0.02413 -0.07466,0.04453 -0.11499,0.0612c-0.04034,0.01667 -0.08191,0.02923 -0.12471,0.0377c-0.04281,0.00853 -0.08603,0.0128 -0.12967,0.0128c-0.04364,0 -0.08686,-0.00423 -0.12966,-0.0127c-0.04281,-0.00847 -0.08438,-0.02103 -0.12472,-0.0377c-0.04034,-0.01667 -0.07867,-0.03707 -0.115,-0.0612c-0.03633,-0.0242 -0.06997,-0.0517 -0.1009,-0.0825l-3.96,-3.93998c-0.03103,-0.03087 -0.05876,-0.06448 -0.08317,-0.10081c-0.02441,-0.03634 -0.04505,-0.07471 -0.0619,-0.1151c-0.01685,-0.0404 -0.0296,-0.08206 -0.03825,-0.12497c-0.00865,-0.04291 -0.01303,-0.08626 -0.01314,-0.13003c-0.00011,-0.04377 0.00405,-0.08714 0.01248,-0.13009c0.00843,-0.04295 0.02097,-0.08467 0.03762,-0.12516c0.01665,-0.04048 0.03709,-0.07895 0.06132,-0.11541c0.02423,-0.03646 0.05178,-0.0702 0.08265,-0.10123c0.03087,-0.03103 0.06448,-0.05876 0.10081,-0.08317c0.03634,-0.02441 0.07471,-0.04505 0.11511,-0.0619c0.04039,-0.01685 0.08205,-0.0296 0.12496,-0.03825c0.04291,-0.00865 0.08626,-0.01303 0.13003,-0.01314c0.04377,-0.00011 0.08714,0.00405 0.13009,0.01248c0.04295,0.00843 0.08467,0.02097 0.12516,0.03762c0.04048,0.01665 0.07895,0.03709 0.11541,0.06132c0.03646,0.02423 0.07021,0.05178 0.10124,0.08265l3.48968,3.47207l8.23978,-8.20196c0.031,-0.03088 0.06473,-0.05844 0.1012,-0.08268c0.03647,-0.02423 0.07493,-0.04468 0.1154,-0.06134c0.04047,-0.01666 0.0822,-0.02921 0.1252,-0.03765c0.04293,-0.00844 0.0863,-0.01261 0.1301,-0.01251c0.04373,0.0001 0.08707,0.00447 0.13,0.01311c0.04293,0.00864 0.0846,0.02138 0.125,0.03822c0.0404,0.01685 0.07877,0.03747 0.1151,0.06188c0.03633,0.0244 0.06993,0.05211 0.1008,0.08314c0.0624,0.06265 0.1104,0.13486 0.144,0.21661c0.03367,0.08175 0.0504,0.16683 0.0502,0.25524c-0.0002,0.08841 -0.0173,0.17341 -0.0513,0.25501c-0.03407,0.08159 -0.08243,0.15357 -0.1451,0.21594l-8.70996,8.66999c-0.03093,0.0308 -0.06455,0.0583 -0.10087,0.0825z"></path></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -100,13 +100,7 @@ const ar: PartialLocaleType = {
Settings: { Settings: {
Title: "الإعدادات", Title: "الإعدادات",
SubTitle: "جميع الإعدادات", SubTitle: "جميع الإعدادات",
Actions: {
ClearAll: "مسح جميع البيانات",
ResetAll: "إعادة تعيين جميع الإعدادات",
Close: "إغلاق",
ConfirmResetAll: "هل أنت متأكد من رغبتك في إعادة تعيين جميع الإعدادات؟",
ConfirmClearAll: "هل أنت متأكد من رغبتك في مسح جميع البيانات؟",
},
Lang: { Lang: {
Name: "Language", // تنبيه: إذا كنت ترغب في إضافة ترجمة جديدة، يرجى عدم ترجمة هذه القيمة وتركها "Language" Name: "Language", // تنبيه: إذا كنت ترغب في إضافة ترجمة جديدة، يرجى عدم ترجمة هذه القيمة وتركها "Language"
All: "كل اللغات", All: "كل اللغات",

View File

@ -29,6 +29,7 @@ const cn = {
PinToastContent: "已将 2 条对话固定至预设提示词", PinToastContent: "已将 2 条对话固定至预设提示词",
PinToastAction: "查看", PinToastAction: "查看",
Delete: "删除", Delete: "删除",
Edit: "编辑",
}, },
Commands: { Commands: {
new: "新建聊天", new: "新建聊天",
@ -108,13 +109,21 @@ const cn = {
}, },
Settings: { Settings: {
Title: "设置", Title: "设置",
SubTitle: "设置选项", SubTitle: "所有设置选项",
Actions: {
ClearAll: "清除所有数据", Danger: {
ResetAll: "重置所有选项", Reset: {
Close: "关闭", Title: "重置所有设置",
ConfirmResetAll: "确认重置所有配置?", SubTitle: "重置所有设置项回默认值",
ConfirmClearAll: "确认清除所有数据?", Action: "立即重置",
Confirm: "确认重置所有设置?",
},
Clear: {
Title: "清除所有数据",
SubTitle: "清除所有聊天、设置数据",
Action: "立即清除",
Confirm: "确认清除所有聊天、设置数据?",
},
}, },
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`

View File

@ -61,13 +61,7 @@ const cs: PartialLocaleType = {
Settings: { Settings: {
Title: "Nastavení", Title: "Nastavení",
SubTitle: "Všechna nastavení", SubTitle: "Všechna nastavení",
Actions: {
ClearAll: "Vymazat všechna data",
ResetAll: "Obnovit veškeré nastavení",
Close: "Zavřít",
ConfirmResetAll: "Jste si jisti, že chcete obnovit všechna nastavení?",
ConfirmClearAll: "Jste si jisti, že chcete smazat všechna data?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Všechny jazyky", All: "Všechny jazyky",

View File

@ -61,14 +61,7 @@ const de: PartialLocaleType = {
Settings: { Settings: {
Title: "Einstellungen", Title: "Einstellungen",
SubTitle: "Alle Einstellungen", SubTitle: "Alle Einstellungen",
Actions: {
ClearAll: "Alle Daten löschen",
ResetAll: "Alle Einstellungen zurücksetzen",
Close: "Schließen",
ConfirmResetAll:
"Möchten Sie wirklich alle Konfigurationen zurücksetzen?",
ConfirmClearAll: "Möchten Sie wirklich alle Chats zurücksetzen?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Alle Sprachen", All: "Alle Sprachen",

View File

@ -30,6 +30,7 @@ const en: LocaleType = {
PinToastContent: "Pinned 2 messages to contextual prompts", PinToastContent: "Pinned 2 messages to contextual prompts",
PinToastAction: "View", PinToastAction: "View",
Delete: "Delete", Delete: "Delete",
Edit: "Edit",
}, },
Commands: { Commands: {
new: "Start a new chat", new: "Start a new chat",
@ -111,12 +112,19 @@ const en: LocaleType = {
Settings: { Settings: {
Title: "Settings", Title: "Settings",
SubTitle: "All Settings", SubTitle: "All Settings",
Actions: { Danger: {
ClearAll: "Clear All Data", Reset: {
ResetAll: "Reset All Settings", Title: "Reset All Settings",
Close: "Close", SubTitle: "Reset all setting items to default",
ConfirmResetAll: "Are you sure you want to reset all configurations?", Action: "Reset",
ConfirmClearAll: "Are you sure you want to reset all data?", Confirm: "Confirm to reset all settings to default?",
},
Clear: {
Title: "Clear All Data",
SubTitle: "Clear all messages and settings",
Action: "Clear",
Confirm: "Confirm to clear all messages and settings?",
},
}, },
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`

View File

@ -61,13 +61,7 @@ const es: PartialLocaleType = {
Settings: { Settings: {
Title: "Configuración", Title: "Configuración",
SubTitle: "Todas las configuraciones", SubTitle: "Todas las configuraciones",
Actions: {
ClearAll: "Borrar todos los datos",
ResetAll: "Restablecer todas las configuraciones",
Close: "Cerrar",
ConfirmResetAll: "Are you sure you want to reset all configurations?",
ConfirmClearAll: "Are you sure you want to reset all chat?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Todos los idiomas", All: "Todos los idiomas",

View File

@ -61,14 +61,7 @@ const fr: PartialLocaleType = {
Settings: { Settings: {
Title: "Paramètres", Title: "Paramètres",
SubTitle: "Toutes les configurations", SubTitle: "Toutes les configurations",
Actions: {
ClearAll: "Effacer toutes les données",
ResetAll: "Réinitialiser les configurations",
Close: "Fermer",
ConfirmResetAll:
"Êtes-vous sûr de vouloir réinitialiser toutes les configurations?",
ConfirmClearAll: "Êtes-vous sûr de vouloir supprimer toutes les données?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION : si vous souhaitez ajouter une nouvelle traduction, ne traduisez pas cette valeur, laissez-la sous forme de `Language` Name: "Language", // ATTENTION : si vous souhaitez ajouter une nouvelle traduction, ne traduisez pas cette valeur, laissez-la sous forme de `Language`
All: "Toutes les langues", All: "Toutes les langues",

View File

@ -61,13 +61,7 @@ const it: PartialLocaleType = {
Settings: { Settings: {
Title: "Impostazioni", Title: "Impostazioni",
SubTitle: "Tutte le impostazioni", SubTitle: "Tutte le impostazioni",
Actions: {
ClearAll: "Cancella tutti i dati",
ResetAll: "Resetta tutte le impostazioni",
Close: "Chiudi",
ConfirmResetAll: "Sei sicuro vuoi cancellare tutte le impostazioni?",
ConfirmClearAll: "Sei sicuro vuoi cancellare tutte le chat?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Tutte le lingue", All: "Tutte le lingue",

View File

@ -61,13 +61,7 @@ const jp: PartialLocaleType = {
Settings: { Settings: {
Title: "設定", Title: "設定",
SubTitle: "設定オプション", SubTitle: "設定オプション",
Actions: {
ClearAll: "すべてのデータをクリア",
ResetAll: "すべてのオプションをリセット",
Close: "閉じる",
ConfirmResetAll: "すべての設定をリセットしてもよろしいですか?",
ConfirmClearAll: "すべてのチャットをリセットしてもよろしいですか?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "全ての言語", All: "全ての言語",

View File

@ -61,13 +61,7 @@ const ko: PartialLocaleType = {
Settings: { Settings: {
Title: "설정", Title: "설정",
SubTitle: "모든 설정", SubTitle: "모든 설정",
Actions: {
ClearAll: "모든 데이터 지우기",
ResetAll: "모든 설정 초기화",
Close: "닫기",
ConfirmResetAll: "모든 설정을 초기화하시겠습니까?",
ConfirmClearAll: "모든 데이터를 지우시겠습니까?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "All Languages", All: "All Languages",

View File

@ -56,11 +56,7 @@ const no: PartialLocaleType = {
Settings: { Settings: {
Title: "Innstillinger", Title: "Innstillinger",
SubTitle: "Alle innstillinger", SubTitle: "Alle innstillinger",
Actions: {
ClearAll: "Fjern alle data",
ResetAll: "Nullstill innstillinger",
Close: "Lukk",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
}, },

View File

@ -61,13 +61,7 @@ const ru: PartialLocaleType = {
Settings: { Settings: {
Title: "Настройки", Title: "Настройки",
SubTitle: "Все настройки", SubTitle: "Все настройки",
Actions: {
ClearAll: "Очистить все данные",
ResetAll: "Сбросить все настройки",
Close: "Закрыть",
ConfirmResetAll: "Вы уверены, что хотите сбросить все настройки?",
ConfirmClearAll: "Вы уверены, что хотите очистить все данные?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Все языки", All: "Все языки",

View File

@ -61,13 +61,7 @@ const tr: PartialLocaleType = {
Settings: { Settings: {
Title: "Ayarlar", Title: "Ayarlar",
SubTitle: "Tüm Ayarlar", SubTitle: "Tüm Ayarlar",
Actions: {
ClearAll: "Tüm Verileri Temizle",
ResetAll: "Tüm Ayarları Sıfırla",
Close: "Kapat",
ConfirmResetAll: "Tüm ayarları sıfırlamak istediğinizden emin misiniz?",
ConfirmClearAll: "Tüm sohbeti sıfırlamak istediğinizden emin misiniz?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Tüm Diller", All: "Tüm Diller",

View File

@ -59,13 +59,7 @@ const tw: PartialLocaleType = {
Settings: { Settings: {
Title: "設定", Title: "設定",
SubTitle: "設定選項", SubTitle: "設定選項",
Actions: {
ClearAll: "清除所有資料",
ResetAll: "重設所有設定",
Close: "關閉",
ConfirmResetAll: "您確定要重設所有設定嗎?",
ConfirmClearAll: "您確定要清除所有数据嗎?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "所有语言", All: "所有语言",

View File

@ -61,13 +61,7 @@ const vi: PartialLocaleType = {
Settings: { Settings: {
Title: "Cài đặt", Title: "Cài đặt",
SubTitle: "Tất cả cài đặt", SubTitle: "Tất cả cài đặt",
Actions: {
ClearAll: "Xóa toàn bộ dữ liệu",
ResetAll: "Khôi phục cài đặt gốc",
Close: "Đóng",
ConfirmResetAll: "Bạn chắc chắn muốn thiết lập lại tất cả cài đặt?",
ConfirmClearAll: "Bạn chắc chắn muốn thiết lập lại tất cả dữ liệu?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Tất cả ngôn ngữ", All: "Tất cả ngôn ngữ",

View File

@ -304,6 +304,9 @@ pre {
&:hover { &:hover {
filter: brightness(0.9); filter: brightness(0.9);
} }
&:focus {
filter: brightness(0.95);
}
} }
.error { .error {

View File

@ -8,7 +8,12 @@ export function trimTopic(topic: string) {
export async function copyToClipboard(text: string) { export async function copyToClipboard(text: string) {
try { try {
if (window.__TAURI__) {
window.__TAURI__.writeText(text);
} else {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
}
showToast(Locale.Copy.Success); showToast(Locale.Copy.Success);
} catch (error) { } catch (error) {
const textArea = document.createElement("textarea"); const textArea = document.createElement("textarea");

View File

@ -17,7 +17,7 @@ tauri-build = { version = "1.3.0", features = [] }
[dependencies] [dependencies]
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.3.0", features = ["clipboard-all", "shell-open", "updater", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-icon", "window-set-ignore-cursor-events", "window-set-resizable", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] } tauri = { version = "1.3.0", features = ["clipboard-all", "dialog-all", "shell-open", "updater", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-icon", "window-set-ignore-cursor-events", "window-set-resizable", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
[features] [features]

View File

@ -4,11 +4,12 @@
"beforeBuildCommand": "yarn export", "beforeBuildCommand": "yarn export",
"beforeDevCommand": "yarn export:dev", "beforeDevCommand": "yarn export:dev",
"devPath": "http://localhost:3000", "devPath": "http://localhost:3000",
"distDir": "../out" "distDir": "../out",
"withGlobalTauri": true
}, },
"package": { "package": {
"productName": "chatgpt-next-web", "productName": "chatgpt-next-web",
"version": "2.8.4" "version": "2.8.5"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {
@ -17,8 +18,18 @@
"all": false, "all": false,
"open": true "open": true
}, },
"dialog": {
"all": true,
"ask": true,
"confirm": true,
"message": true,
"open": true,
"save": true
},
"clipboard": { "clipboard": {
"all": true "all": true,
"writeText": true,
"readText": true
}, },
"window": { "window": {
"all": false, "all": false,