Skip to content

Commit

Permalink
update markdown editor
Browse files Browse the repository at this point in the history
  • Loading branch information
zmh-program committed Oct 3, 2023
1 parent 1c4e613 commit b9d268a
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 15 deletions.
93 changes: 93 additions & 0 deletions app/src/assets/editor.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
.editor-action {
position: absolute;
top: 50%;
right: 8px;
transform: translateY(-50%);
background: hsl(var(--input)) !important;
padding: 6px;
border-radius: 50%;
cursor: pointer;
transition: 0.1s;
outline: 0;
opacity: 0;

&.active {
opacity: 1;
}

&:hover {
background: hsl(var(--border-hover)) !important;
}
}

.editor-dialog {
max-width: min(90vw, 920px) !important;
}

.editor-container {
padding: 2px 4px 0;
}

.editor-wrapper {
padding: 4px 0;
}

.editor-object {
position: relative;
display: grid;
grid-gap: 12px;
height: 100%;

&.show-editor {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
grid-template-areas: 'editor';
}

&.show-preview {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
grid-template-areas: 'markdown';
}

&.show-editor.show-preview {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
grid-template-areas: 'editor markdown';
}
}

.editor-input {
grid-area: editor;
scrollbar-width: thin;
height: max-content;
transition: .1s;
min-height: 20vh !important;
color: hsl(var(--text));
font-size: 16px !important;
padding: 14px 12px !important;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
}

.editor-preview {
height: 100%;
grid-area: markdown;
overflow: auto;
padding: 12px;
border-radius: 4px;
background: hsl(var(--input));
color: hsl(var(--text));
border: 1px solid hsl(var(--border));
scrollbar-width: thin;
transition: 0.1s;
min-height: 20vh !important;
font-size: 16px;
}

.editor-toolbar {
display: flex;
flex-direction: row;
align-items: flex-end;
gap: 4px;
margin: 4px 0;
}
2 changes: 1 addition & 1 deletion app/src/assets/file.less
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.file-action {
position: absolute;
top: 50%;
left: 6px;
left: 8px;
transform: translateY(-50%);
background: hsl(var(--input)) !important;
padding: 6px;
Expand Down
1 change: 1 addition & 0 deletions app/src/assets/home.less
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@
width: 100%;
text-align: center;
color: hsl(var(--text));
white-space: pre-wrap;

&::placeholder {
color: hsl(var(--text-secondary));
Expand Down
148 changes: 148 additions & 0 deletions app/src/components/RichEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog.tsx";
import { Edit, Image, MenuSquare, PanelRight } from "lucide-react";
import { useTranslation } from "react-i18next";
import "../assets/editor.less";
import { Textarea } from "./ui/textarea.tsx";
import Markdown from "./Markdown.tsx";
import { useEffect, useRef, useState } from "react";
import { Toggle } from "./ui/toggle.tsx";
import {mobile} from "../utils.ts";

type RichEditorProps = {
value: string;
onChange: (value: string) => void;
className?: string;
id?: string;
placeholder?: string;
maxLength?: number;
};

function RichEditor({
value,
onChange,
className,
id,
placeholder,
maxLength,
}: RichEditorProps) {
const { t } = useTranslation();
const input = useRef(null);
const [openPreview, setOpenPreview] = useState(!mobile);
const [openInput, setOpenInput] = useState(true);

useEffect(() => {
if (!input.current) return;
const target = input.current as HTMLElement;
const preview = target.parentElement?.querySelector(
".editor-preview",
) as HTMLElement;

const listener = () => {
preview.style.height = `${target.clientHeight}px`;
};
target.addEventListener("transitionstart", listener);
const task = setInterval(listener, 250);
target.addEventListener("scroll", () => {
preview.scrollTop = target.scrollTop;
});

preview.style.height = `${target.clientHeight}px`;

return () => {
target.removeEventListener("transitionstart", listener);
clearInterval(task);
};
}, [input]);

return (
<>
<Dialog>
<DialogTrigger asChild>
<div
className={`editor-action active ${className}`}
>
<Edit className={`h-3.5 w-3.5`} />
</div>
</DialogTrigger>
<DialogContent className={`editor-dialog flex-dialog`}>
<DialogHeader>
<DialogTitle>{t("edit")}</DialogTitle>
<DialogDescription asChild>
<div className={`editor-container`}>
<div className={`editor-toolbar`}>
<div className={`grow`} />
<Toggle
variant={`outline`}
className={`h-8 w-8 p-0`}
pressed={openInput && !openPreview}
onClick={() => {
setOpenPreview(false);
setOpenInput(true);
}}
>
<MenuSquare className={`h-3.5 w-3.5`} />
</Toggle>

<Toggle
variant={`outline`}
className={`h-8 w-8 p-0`}
pressed={openInput && openPreview}
onClick={() => {
setOpenPreview(true);
setOpenInput(true);
}}
>
<PanelRight className={`h-3.5 w-3.5`} />
</Toggle>

<Toggle
variant={`outline`}
className={`h-8 w-8 p-0`}
pressed={!openInput && openPreview}
onClick={() => {
setOpenPreview(true);
setOpenInput(false);
}}
>
<Image className={`h-3.5 w-3.5`} />
</Toggle>
</div>
<div className={`editor-wrapper`}>
<div
className={`editor-object ${
openInput ? "show-editor" : ""
} ${openPreview ? "show-preview" : ""}`}
>
{openInput && (
<Textarea
placeholder={placeholder}
value={value}
className={`editor-input`}
id={id}
maxLength={maxLength}
onChange={(e) => onChange(e.target.value)}
ref={input}
/>
)}
{openPreview && (
<Markdown className={`editor-preview`} children={value} />
)}
</div>
</div>
</div>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</>
);
}

export default RichEditor;
2 changes: 1 addition & 1 deletion app/src/conf.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import axios from "axios";

export const version: string = "3.2.1";
export const version: string = "3.2.2";
export const deploy: boolean = true;
export let rest_api: string = "http://localhost:8094";
export let ws_api: string = "ws://localhost:8094";
Expand Down
3 changes: 3 additions & 0 deletions app/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const resources = {
"request-failed":
"Request failed. Please check your network and try again.",
close: "Close",
edit: "Edit",
conversation: {
title: "Conversation",
empty: "Empty",
Expand Down Expand Up @@ -183,6 +184,7 @@ const resources = {
"server-error-prompt": "登录出错,请重试。",
"request-failed": "请求失败,请检查您的网络并重试。",
close: "关闭",
edit: "编辑",
conversation: {
title: "会话",
empty: "空空如也",
Expand Down Expand Up @@ -333,6 +335,7 @@ const resources = {
"request-failed":
"Ошибка запроса. Пожалуйста, проверьте свою сеть и попробуйте еще раз.",
close: "Закрыть",
edit: "Редактировать",
conversation: {
title: "Разговор",
empty: "Пусто",
Expand Down
38 changes: 25 additions & 13 deletions app/src/routes/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { setMenu } from "../store/menu.ts";
import FileProvider, { FileObject } from "../components/FileProvider.tsx";
import router from "../router.ts";
import SelectGroup from "../components/SelectGroup.tsx";
import RichEditor from "../components/RichEditor.tsx";

function SideBar() {
const { t } = useTranslation();
Expand Down Expand Up @@ -283,6 +284,7 @@ function ChatWrapper() {
content: "",
});
const [clearEvent, setClearEvent] = useState<() => void>(() => {});
const [input, setInput] = useState("");
const dispatch = useDispatch();
const init = useSelector(selectInit);
const auth = useSelector(selectAuthenticated);
Expand Down Expand Up @@ -318,10 +320,8 @@ function ChatWrapper() {

async function handleSend(auth: boolean, model: string, web: boolean) {
// because of the function wrapper, we need to update the selector state using props.
if (!target.current) return;
const el = target.current as HTMLInputElement;
if (await processSend(el.value, auth, model, web)) {
el.value = "";
if (await processSend(input, auth, model, web)) {
setInput("");
}
}

Expand Down Expand Up @@ -377,15 +377,6 @@ function ChatWrapper() {
</Tooltip>
</TooltipProvider>
<div className={`chat-box`}>
<Input
id={`input`}
className={`input-box`}
ref={target}
placeholder={t("chat.placeholder")}
onKeyDown={async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") await handleSend(auth, model, web);
}}
/>
{auth && (
<FileProvider
id={`file`}
Expand All @@ -395,6 +386,27 @@ function ChatWrapper() {
setClearEvent={setClearEvent}
/>
)}
<Input
id={`input`}
className={`input-box`}
ref={target}
value={input}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setInput(e.target.value)
}
placeholder={t("chat.placeholder")}
onKeyDown={async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") await handleSend(auth, model, web);
}}
/>
<RichEditor
value={input}
onChange={setInput}
className={`editor`}
id={`editor`}
placeholder={t("chat.placeholder")}
maxLength={8000}
/>
</div>
<Button
size={`icon`}
Expand Down

0 comments on commit b9d268a

Please sign in to comment.