This commit is contained in:
GH Action - Upstream Sync
2023-07-10 01:23:56 +00:00
27 changed files with 498 additions and 270 deletions

View File

@@ -26,7 +26,7 @@ export function ChatItem(props: {
count: number;
time: string;
selected: boolean;
id: number;
id: string;
index: number;
narrow?: boolean;
mask: Mask;

View File

@@ -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 {

View File

@@ -221,9 +221,11 @@ function useSubmitHandler() {
};
}
export type RenderPompt = Pick<Prompt, "title" | "content">;
export function PromptHints(props: {
prompts: Prompt[];
onPromptSelect: (prompt: Prompt) => void;
prompts: RenderPompt[];
onPromptSelect: (prompt: RenderPompt) => void;
}) {
const noPrompts = props.prompts.length === 0;
const [selectIndex, setSelectIndex] = useState(0);
@@ -412,8 +414,12 @@ export function ChatActions(props: {
// switch model
const currentModel = chatStore.currentSession().mask.modelConfig.model;
const models = useMemo(
() => config.models.filter((m) => m.available).map((m) => m.name),
[config.models],
() =>
config
.allModels()
.filter((m) => m.available)
.map((m) => m.name),
[config],
);
const [showModelSelector, setShowModelSelector] = useState(false);
@@ -542,7 +548,7 @@ export function Chat() {
// prompt hints
const promptStore = usePromptStore();
const [promptHints, setPromptHints] = useState<Prompt[]>([]);
const [promptHints, setPromptHints] = useState<RenderPompt[]>([]);
const onSearch = useDebouncedCallback(
(text: string) => {
const matchedPrompts = promptStore.search(text);
@@ -624,7 +630,7 @@ export function Chat() {
setAutoScroll(true);
};
const onPromptSelect = (prompt: Prompt) => {
const onPromptSelect = (prompt: RenderPompt) => {
setTimeout(() => {
setPromptHints([]);
@@ -642,8 +648,8 @@ export function Chat() {
};
// stop response
const onUserStop = (messageId: number) => {
ChatControllerPool.stop(sessionIndex, messageId);
const onUserStop = (messageId: string) => {
ChatControllerPool.stop(session.id, messageId);
};
useEffect(() => {
@@ -703,54 +709,51 @@ export function Chat() {
}
};
const findLastUserIndex = (messageId: number) => {
const findLastUserIndex = (messageId: string) => {
// find last user input message and resend
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?: string) => {
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: string) => {
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 +926,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 +944,38 @@ export function Chat() {
}
>
<div className={styles["chat-message-container"]}>
<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,
10,
);
chatStore.updateCurrentSession((session) => {
const m = session.messages.find(
(m) => m.id === message.id,
<div className={styles["chat-message-header"]}>
<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,
10,
);
if (m) {
m.content = newMessage;
}
});
}}
></IconButton>
chatStore.updateCurrentSession((session) => {
const m = session.messages.find(
(m) => m.id === message.id,
);
if (m) {
m.content = newMessage;
}
});
}}
></IconButton>
</div>
{isUser ? (
<Avatar avatar={config.avatar} />
) : (
<MaskAvatar mask={session.mask} />
)}
</div>
{isUser ? (
<Avatar avatar={config.avatar} />
) : (
<MaskAvatar mask={session.mask} />
)}
</div>
{showTyping && (
<div className={styles["chat-message-status"]}>
{Locale.Chat.Typing}
</div>
)}
<div className={styles["chat-message-item"]}>
<Markdown
content={message.content}
loading={
(message.preview || message.content.length === 0) &&
!isUser
}
onContextMenu={(e) => onRightClick(e, message)}
onDoubleClickCapture={() => {
if (!isMobileScreen) return;
setUserInput(message.content);
}}
fontSize={fontSize}
parentRef={scrollRef}
defaultShow={i >= messages.length - 10}
/>
{showActions && (
<div className={styles["chat-message-actions"]}>
<div
className={styles["chat-input-actions"]}
style={{
marginTop: 10,
marginBottom: 0,
}}
>
<div className={styles["chat-input-actions"]}>
{message.streaming ? (
<ChatAction
text={Locale.Chat.Actions.Stop}
@@ -1010,7 +987,7 @@ export function Chat() {
<ChatAction
text={Locale.Chat.Actions.Retry}
icon={<ResetIcon />}
onClick={() => onResend(message.id ?? i)}
onClick={() => onResend(message)}
/>
<ChatAction
@@ -1035,12 +1012,34 @@ export function Chat() {
</div>
)}
</div>
{showActions && (
<div className={styles["chat-message-action-date"]}>
{message.date.toLocaleString()}
{showTyping && (
<div className={styles["chat-message-status"]}>
{Locale.Chat.Typing}
</div>
)}
<div className={styles["chat-message-item"]}>
<Markdown
content={message.content}
loading={
(message.preview || message.content.length === 0) &&
!isUser
}
onContextMenu={(e) => onRightClick(e, message)}
onDoubleClickCapture={() => {
if (!isMobileScreen) return;
setUserInput(message.content);
}}
fontSize={fontSize}
parentRef={scrollRef}
defaultShow={i >= messages.length - 10}
/>
</div>
<div className={styles["chat-message-action-date"]}>
{isContext
? Locale.Chat.IsContext
: message.date.toLocaleString()}
</div>
</div>
</div>
{shouldShowClearContextDivider && <ClearContextDivider />}

View File

@@ -8,7 +8,6 @@ import {
Modal,
Select,
showImageModal,
showModal,
showToast,
} from "./ui-lib";
import { IconButton } from "./button";
@@ -149,7 +148,7 @@ export function MessageExporter() {
if (exportConfig.includeContext) {
ret.push(...session.mask.context);
}
ret.push(...session.messages.filter((m, i) => selection.has(m.id ?? i)));
ret.push(...session.messages.filter((m, i) => selection.has(m.id)));
return ret;
}, [
exportConfig.includeContext,
@@ -244,9 +243,10 @@ export function RenderExport(props: {
return;
}
const renderMsgs = messages.map((v) => {
const renderMsgs = messages.map((v, i) => {
const [_, role] = v.id.split(":");
return {
id: i.toString(),
role: role as any,
content: v.innerHTML,
date: "",

View File

@@ -13,7 +13,13 @@ import EyeIcon from "../icons/eye.svg";
import CopyIcon from "../icons/copy.svg";
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
import { ChatMessage, ModelConfig, useAppConfig, useChatStore } from "../store";
import {
ChatMessage,
createMessage,
ModelConfig,
useAppConfig,
useChatStore,
} from "../store";
import { ROLES } from "../client/api";
import {
Input,
@@ -35,6 +41,7 @@ import { Updater } from "../typing";
import { ModelConfigList } from "./model-config";
import { FileName, Path } from "../constant";
import { BUILTIN_MASK_STORE } from "../masks";
import { nanoid } from "nanoid";
export function MaskAvatar(props: { mask: Mask }) {
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
@@ -279,11 +286,13 @@ export function ContextPrompts(props: {
bordered
className={chatStyle["context-prompt-button"]}
onClick={() =>
addContextPrompt({
role: "user",
content: "",
date: "",
})
addContextPrompt(
createMessage({
role: "user",
content: "",
date: "",
}),
)
}
/>
</div>
@@ -319,7 +328,7 @@ export function MaskPage() {
}
};
const [editingMaskId, setEditingMaskId] = useState<number | undefined>();
const [editingMaskId, setEditingMaskId] = useState<string | undefined>();
const editingMask =
maskStore.get(editingMaskId) ?? BUILTIN_MASK_STORE.get(editingMaskId);
const closeMaskModal = () => setEditingMaskId(undefined);

View File

@@ -51,9 +51,9 @@ function useShiftRange() {
}
export function useMessageSelector() {
const [selection, setSelection] = useState(new Set<number>());
const updateSelection: Updater<Set<number>> = (updater) => {
const newSelection = new Set<number>(selection);
const [selection, setSelection] = useState(new Set<string>());
const updateSelection: Updater<Set<string>> = (updater) => {
const newSelection = new Set<string>(selection);
updater(newSelection);
setSelection(newSelection);
};
@@ -65,8 +65,8 @@ export function useMessageSelector() {
}
export function MessageSelector(props: {
selection: Set<number>;
updateSelection: Updater<Set<number>>;
selection: Set<string>;
updateSelection: Updater<Set<string>>;
defaultSelectAll?: boolean;
onSelected?: (messages: ChatMessage[]) => void;
}) {
@@ -83,12 +83,12 @@ export function MessageSelector(props: {
const config = useAppConfig();
const [searchInput, setSearchInput] = useState("");
const [searchIds, setSearchIds] = useState(new Set<number>());
const isInSearchResult = (id: number) => {
const [searchIds, setSearchIds] = useState(new Set<string>());
const isInSearchResult = (id: string) => {
return searchInput.length === 0 || searchIds.has(id);
};
const doSearch = (text: string) => {
const searchResults = new Set<number>();
const searchResults = new Set<string>();
if (text.length > 0) {
messages.forEach((m) =>
m.content.includes(text) ? searchResults.add(m.id!) : null,

View File

@@ -24,8 +24,8 @@ export function ModelConfigList(props: {
);
}}
>
{config.models.map((v) => (
<option value={v.name} key={v.name} disabled={!v.available}>
{config.allModels().map((v, i) => (
<option value={v.name} key={i} disabled={!v.available}>
{v.name}
</option>
))}

View File

@@ -103,8 +103,7 @@ export function NewChat() {
useCommand({
mask: (id) => {
try {
const intId = parseInt(id);
const mask = maskStore.get(intId) ?? BUILTIN_MASK_STORE.get(intId);
const mask = maskStore.get(id) ?? BUILTIN_MASK_STORE.get(id);
startChat(mask ?? undefined);
} catch {
console.error("[New Chat] failed to create chat from mask id=", id);

View File

@@ -48,8 +48,9 @@ import { useNavigate } from "react-router-dom";
import { Avatar, AvatarPicker } from "./emoji";
import { getClientConfig } from "../config/client";
import { useSyncStore } from "../store/sync";
import { nanoid } from "nanoid";
function EditPromptModal(props: { id: number; onClose: () => void }) {
function EditPromptModal(props: { id: string; onClose: () => void }) {
const promptStore = usePromptStore();
const prompt = promptStore.get(props.id);
@@ -107,7 +108,7 @@ function UserPromptModal(props: { onClose?: () => void }) {
const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
const [editingPromptId, setEditingPromptId] = useState<number>();
const [editingPromptId, setEditingPromptId] = useState<string>();
useEffect(() => {
if (searchInput.length > 0) {
@@ -128,6 +129,8 @@ function UserPromptModal(props: { onClose?: () => void }) {
key="add"
onClick={() =>
promptStore.add({
id: nanoid(),
createdAt: Date.now(),
title: "Empty Prompt",
content: "Empty Prompt Content",
})
@@ -315,7 +318,6 @@ export function Settings() {
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const config = useAppConfig();
const updateConfig = config.update;
const chatStore = useChatStore();
const updateStore = useUpdateStore();
const [checkingUpdate, setCheckingUpdate] = useState(false);
@@ -579,6 +581,38 @@ export function Settings() {
</ListItem>
</List>
<List>
<ListItem
title={Locale.Settings.Prompt.Disable.Title}
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
>
<input
type="checkbox"
checked={config.disablePromptHint}
onChange={(e) =>
updateConfig(
(config) =>
(config.disablePromptHint = e.currentTarget.checked),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Prompt.List}
subTitle={Locale.Settings.Prompt.ListCount(
builtinCount,
customCount,
)}
>
<IconButton
icon={<EditIcon />}
text={Locale.Settings.Prompt.Edit}
onClick={() => setShowPromptModal(true)}
/>
</ListItem>
</List>
<List>
{showAccessCode ? (
<ListItem
@@ -654,38 +688,22 @@ export function Settings() {
)}
</ListItem>
) : null}
</List>
<List>
<ListItem
title={Locale.Settings.Prompt.Disable.Title}
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
title={Locale.Settings.CustomModel.Title}
subTitle={Locale.Settings.CustomModel.SubTitle}
>
<input
type="checkbox"
checked={config.disablePromptHint}
type="text"
value={config.customModels}
placeholder="model1,model2,model3"
onChange={(e) =>
updateConfig(
(config) =>
(config.disablePromptHint = e.currentTarget.checked),
config.update(
(config) => (config.customModels = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Prompt.List}
subTitle={Locale.Settings.Prompt.ListCount(
builtinCount,
customCount,
)}
>
<IconButton
icon={<EditIcon />}
text={Locale.Settings.Prompt.Edit}
onClick={() => setShowPromptModal(true)}
/>
</ListItem>
</List>
<SyncItems />

View File

@@ -286,7 +286,9 @@
&-content {
.list {
overflow: hidden;
max-height: 90vh;
overflow-x: hidden;
overflow-y: auto;
.list-item {
cursor: pointer;