feat: support edit message and delete message (#51) and improve working state

This commit is contained in:
Zhang Minghan 2024-01-29 12:15:33 +08:00
parent a3a9e5a525
commit fc391d28b0
14 changed files with 123 additions and 44 deletions

View File

@ -76,7 +76,8 @@ export class Conversation {
const idx = ev.index ?? -1;
if (this.isValidIndex(idx)) {
delete this.data[idx];
this.data.splice(idx, 1);
this.sendRemoveEvent(idx);
this.triggerCallback();
}
@ -116,21 +117,11 @@ export class Conversation {
}
public sendEditEvent(id: number, message: string) {
this.sendEvent(
"edit",
JSON.stringify({
message: `${id}:${message}`,
}),
);
this.sendEvent("edit", `${id}:${message}`);
}
public sendRemoveEvent(id: number) {
this.sendEvent(
"remove",
JSON.stringify({
message: `${id}`,
}),
);
this.sendEvent("remove", id.toString());
}
public sendShareEvent(refer: string) {

View File

@ -81,6 +81,10 @@ strong {
display: flex;
flex-direction: row;
align-items: center;
&.gold {
color: hsl(var(--gold));
}
}
.flex-dialog {

View File

@ -446,7 +446,7 @@
transition: 0.2s;
opacity: 0;
border: 1px solid var(--border);
outline: 0;
outline: none;
&:hover {
color: hsl(var(--text));

View File

@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next";
import "@/assets/common/editor.less";
import { Textarea } from "./ui/textarea.tsx";
import Markdown from "./Markdown.tsx";
import { useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { Toggle } from "./ui/toggle.tsx";
import { mobile } from "@/utils/device.ts";
import { Button } from "./ui/button.tsx";
@ -22,9 +22,25 @@ type RichEditorProps = {
value: string;
onChange: (value: string) => void;
maxLength?: number;
open?: boolean;
setOpen?: (open: boolean) => void;
children?: React.ReactNode;
submittable?: boolean;
onSubmit?: (value: string) => void;
closeOnSubmit?: boolean;
};
function RichEditor({ value, onChange, maxLength }: RichEditorProps) {
function RichEditor({
value,
onChange,
maxLength,
submittable,
onSubmit,
setOpen,
closeOnSubmit,
}: RichEditorProps) {
const { t } = useTranslation();
const input = useRef(null);
const [openPreview, setOpenPreview] = useState(!mobile);
@ -113,7 +129,7 @@ function RichEditor({ value, onChange, maxLength }: RichEditorProps) {
>
{openInput && (
<Textarea
placeholder={t("chat.placeholder")}
placeholder={t("chat.placeholder-raw")}
value={value}
className={`editor-input`}
id={`editor`}
@ -127,6 +143,26 @@ function RichEditor({ value, onChange, maxLength }: RichEditorProps) {
)}
</div>
</div>
{submittable && (
<div className={`editor-footer mt-2 flex flex-row`}>
<Button
variant={`outline`}
className={`ml-auto mr-2`}
onClick={() => setOpen?.(false)}
>
{t("cancel")}
</Button>
<Button
variant={`default`}
onClick={() => {
onSubmit?.(value);
(closeOnSubmit ?? true) && setOpen?.(false);
}}
>
{t("submit")}
</Button>
</div>
)}
</div>
);
}
@ -136,12 +172,16 @@ function EditorProvider(props: RichEditorProps) {
return (
<>
<Dialog>
<DialogTrigger asChild>
<ChatAction text={t("editor")}>
<Maximize className={`h-4 w-4`} />
</ChatAction>
</DialogTrigger>
<Dialog open={props.open} onOpenChange={props.setOpen}>
{!props.setOpen && (
<DialogTrigger asChild>
{props.children ?? (
<ChatAction text={t("editor")}>
<Maximize className={`h-4 w-4`} />
</ChatAction>
)}
</DialogTrigger>
)}
<DialogContent className={`editor-dialog flex-dialog`}>
<DialogHeader>
<DialogTitle>{t("edit")}</DialogTitle>

View File

@ -1,6 +1,7 @@
import { Message } from "@/api/types.ts";
import Markdown from "@/components/Markdown.tsx";
import {
CalendarCheck2,
CircleSlash,
Cloud,
CloudFog,
@ -17,7 +18,7 @@ import {
import { filterMessage } from "@/utils/processor.ts";
import { copyClipboard, saveAsFile, useInputValue } from "@/utils/dom.ts";
import { useTranslation } from "react-i18next";
import { Ref, useMemo, useRef } from "react";
import { Ref, useMemo, useRef, useState } from "react";
import {
DropdownMenu,
DropdownMenuContent,
@ -26,6 +27,7 @@ import {
} from "@/components/ui/dropdown-menu.tsx";
import { cn } from "@/components/ui/lib/utils.ts";
import Tips from "@/components/Tips.tsx";
import EditorProvider from "@/components/EditorProvider.tsx";
type MessageProps = {
index: number;
@ -68,8 +70,18 @@ function MessageQuota({ message }: MessageQuotaProps) {
return (
message.quota &&
message.quota !== 0 && (
<Tips classNamePopup={`icon-tooltip justify-center`} trigger={trigger}>
<CloudFog className={`h-4 w-4 mr-2`} />
<Tips
classNamePopup={cn(
"icon-tooltip justify-center",
message.plan && "gold",
)}
trigger={trigger}
>
{message.plan ? (
<CalendarCheck2 className={`h-4 w-4 mr-2`} />
) : (
<CloudFog className={`h-4 w-4 mr-2`} />
)}
<p>{message.quota.toFixed(6)}</p>
</Tips>
)
@ -80,6 +92,9 @@ function MessageContent({ message, end, index, onEvent }: MessageProps) {
const { t } = useTranslation();
const isAssistant = message.role === "assistant";
const [open, setOpen] = useState(false);
const [editedMessage, setEditedMessage] = useState<string | undefined>("");
return (
<div
className={cn(
@ -87,6 +102,14 @@ function MessageContent({ message, end, index, onEvent }: MessageProps) {
isAssistant ? "flex-row" : "flex-row-reverse",
)}
>
<EditorProvider
submittable={true}
onSubmit={(value) => onEvent && onEvent("edit", index, value)}
open={open}
setOpen={setOpen}
value={editedMessage ?? ""}
onChange={setEditedMessage}
/>
<div className={`message-content`}>
{message.content.length ? (
<Markdown children={message.content} />
@ -135,11 +158,13 @@ function MessageContent({ message, end, index, onEvent }: MessageProps) {
<MousePointerSquare className={`h-4 w-4 mr-1.5`} />
{t("message.use")}
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem onClick={() => setOpen(true)}>
<PencilLine className={`h-4 w-4 mr-1.5`} />
{t("message.edit")}
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem
onClick={() => onEvent && onEvent("remove", index)}
>
<Trash className={`h-4 w-4 mr-1.5`} />
{t("message.remove")}
</DropdownMenuItem>

View File

@ -8,11 +8,10 @@ import { chatEvent } from "@/events/chat.ts";
import { addEventListeners } from "@/utils/dom.ts";
type ChatInterfaceProps = {
setWorking?: (working: boolean) => void;
setTarget: (target: HTMLDivElement | null) => void;
};
function ChatInterface({ setTarget, setWorking }: ChatInterfaceProps) {
function ChatInterface({ setTarget }: ChatInterfaceProps) {
const ref = React.useRef(null);
const messages: Message[] = useSelector(selectMessages);
const current: number = useSelector(selectCurrent);
@ -29,12 +28,6 @@ function ChatInterface({ setTarget, setWorking }: ChatInterfaceProps) {
[messages],
);
useEffect(() => {
const end = messages[messages.length - 1].end;
const working = messages.length > 0 && !(end === undefined ? true : end);
setWorking?.(working);
}, [messages]);
useEffect(() => {
if (!ref.current) return;
const el = ref.current as HTMLDivElement;

View File

@ -20,7 +20,7 @@ import {
} from "@/components/ui/dialog.tsx";
import { getLanguage } from "@/i18n.ts";
import { selectAuthenticated } from "@/store/auth.ts";
import { docsEndpoint, useDeeptrain } from "@/conf/env.ts";
import { appLogo, docsEndpoint, useDeeptrain } from "@/conf/env.ts";
function ChatSpace() {
const [open, setOpen] = useState(false);
@ -32,6 +32,12 @@ function ChatSpace() {
return (
<div className={`chat-product`}>
<img
src={appLogo}
className={`chat-logo h-20 w-20 translate-y-[-2rem]`}
alt={``}
/>
{useDeeptrain && (
<Button variant={`outline`} onClick={() => setOpen(true)}>
<Users2 className={`h-4 w-4 mr-1.5`} />

View File

@ -46,6 +46,13 @@ type InterfaceProps = {
function Interface(props: InterfaceProps) {
const messages = useSelector(selectMessages);
useEffect(() => {
const end =
messages.length > 0 && (messages[messages.length - 1].end ?? true);
const working = messages.length > 0 && !end;
props.setWorking?.(working);
}, [messages]);
return messages.length > 0 ? <ChatInterface {...props} /> : <ChatSpace />;
}

View File

@ -60,6 +60,7 @@ function ConversationSegment({
}}
>
<DropdownMenuTrigger
className={`outline-none`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();

View File

@ -36,7 +36,7 @@ function ChatInput({
onValueChange(e.target.value);
setMemory("history", e.target.value);
}}
placeholder={t("chat.placeholder")}
placeholder={sender ? t("chat.placeholder-enter") : t("chat.placeholder")}
onKeyDown={async (e) => {
if (e.key === "Enter") {
if (sender || e.ctrlKey) {

View File

@ -43,6 +43,7 @@
"upward": "上移",
"downward": "下移",
"save": "保存",
"submit": "提交",
"announcement": "站点公告",
"i-know": "我已知晓",
"auth": {
@ -125,6 +126,8 @@
"web": "联网搜索",
"web-aria": "切换网络搜索功能",
"placeholder": "写点什么... (Ctrl+Enter 发送)",
"placeholder-enter": "写点什么... (Enter 发送)",
"placeholder-raw": "写点什么...",
"recall": "历史复原",
"recall-desc": "检测到您上次有未发送的消息,已经为您恢复。",
"recall-cancel": "取消"

View File

@ -78,7 +78,9 @@
"placeholder": "Write something... (Ctrl+Enter to send)",
"recall": "History Recall",
"recall-desc": "Detected that you have unsent messages last time, has been restored for you.",
"recall-cancel": "Cancel"
"recall-cancel": "Cancel",
"placeholder-enter": "Write something... (Enter to send)",
"placeholder-raw": "Write something..."
},
"message": {
"copy": "Copy Message",
@ -617,5 +619,6 @@
"downward": "Move down",
"save": "Save",
"announcement": "Site Announcement",
"i-know": "Yes, I understand."
"i-know": "Yes, I understand.",
"submit": "Send"
}

View File

@ -78,7 +78,9 @@
"placeholder": "何か書いてください... Ctrl + Enterで送信",
"recall": "履歴の回復",
"recall-desc": "最後に未送信のメッセージが検出され、復元されました。",
"recall-cancel": "キャンセル"
"recall-cancel": "キャンセル",
"placeholder-enter": "何か書いてください... 送信するにはEnterキーを押してください",
"placeholder-raw": "何か書いてください..."
},
"message": {
"copy": "メッセージをコピー",
@ -617,5 +619,6 @@
"downward": "下へ移動",
"save": "保存",
"announcement": "サイトのお知らせ",
"i-know": "私は知っています"
"i-know": "私は知っています",
"submit": "提出"
}

View File

@ -78,7 +78,9 @@
"placeholder": "Напишите что-нибудь... (Ctrl+Enter для отправки)",
"recall": "История",
"recall-desc": "Обнаружено, что у вас есть неотправленные сообщения в прошлый раз, они были восстановлены для вас.",
"recall-cancel": "Отмена"
"recall-cancel": "Отмена",
"placeholder-enter": "Напишите что-нибудь... (Введите, чтобы отправить)",
"placeholder-raw": "Напишите что-нибудь..."
},
"message": {
"copy": "Копировать сообщение",
@ -617,5 +619,6 @@
"downward": "Ниже",
"save": "Сохранить",
"announcement": "Объявление о площадке",
"i-know": "Мне известно о"
"i-know": "Мне известно о",
"submit": "передавать"
}