mirror of
https://github.com/coaidev/coai.git
synced 2025-05-23 23:10:13 +09:00
feat: support edit message and delete message (#51) and improve working state
This commit is contained in:
parent
a3a9e5a525
commit
fc391d28b0
@ -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) {
|
||||
|
@ -81,6 +81,10 @@ strong {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
&.gold {
|
||||
color: hsl(var(--gold));
|
||||
}
|
||||
}
|
||||
|
||||
.flex-dialog {
|
||||
|
@ -446,7 +446,7 @@
|
||||
transition: 0.2s;
|
||||
opacity: 0;
|
||||
border: 1px solid var(--border);
|
||||
outline: 0;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
color: hsl(var(--text));
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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`} />
|
||||
|
@ -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 />;
|
||||
}
|
||||
|
||||
|
@ -60,6 +60,7 @@ function ConversationSegment({
|
||||
}}
|
||||
>
|
||||
<DropdownMenuTrigger
|
||||
className={`outline-none`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -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) {
|
||||
|
@ -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": "取消"
|
||||
|
@ -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"
|
||||
}
|
@ -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": "提出"
|
||||
}
|
@ -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": "передавать"
|
||||
}
|
Loading…
Reference in New Issue
Block a user