1
0
Code Issues Pull Requests Packages Projects Releases Wiki Activity GitHub Gitee

chore pkg and demo

This commit is contained in:
songjunxi
2023-10-22 10:52:46 +08:00
commit bccff40c0d
59 changed files with 12213 additions and 0 deletions

View File

@@ -0,0 +1,223 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useEditor, EditorContent, JSONContent } from "@tiptap/react";
import { defaultEditorProps } from "./props";
import { defaultExtensions } from "./extensions";
import useLocalStorage from "@/lib/hooks/use-local-storage";
import { useDebouncedCallback } from "use-debounce";
import { useCompletion } from "ai/react";
import { toast } from "sonner";
import va from "@vercel/analytics";
import { defaultEditorContent } from "./default-content";
import { EditorBubbleMenu } from "./bubble-menu";
import { getPrevText } from "@/lib/editor";
import { ImageResizer } from "./extensions/image-resizer";
import { EditorProps } from "@tiptap/pm/view";
import { Editor as EditorClass, Extensions } from "@tiptap/core";
import { NovelContext } from "./provider";
import "./styles.css";
import AIEditorBubble from "./bubble-menu/ai-selectors/edit/ai-edit-bubble";
import AIGeneratingLoading from "./bubble-menu/ai-selectors/ai-loading";
import AITranslateBubble from "./bubble-menu/ai-selectors/translate/ai-translate-bubble";
export default function Editor({
completionApi = "/api/generate",
className = "novel-relative novel-min-h-[500px] novel-w-full novel-max-w-screen-lg novel-border-stone-200 novel-bg-white sm:novel-mb-[calc(20vh)] sm:novel-rounded-lg sm:novel-border sm:novel-shadow-lg",
defaultValue = defaultEditorContent,
extensions = [],
editorProps = {},
onUpdate = () => {},
onDebouncedUpdate = () => {},
debounceDuration = 750,
storageKey = "novel__content",
disableLocalStorage = false,
editable = true,
plan = "5",
}: {
/**
* The API route to use for the OpenAI completion API.
* Defaults to "/api/generate".
*/
completionApi?: string;
/**
* Additional classes to add to the editor container.
* Defaults to "relative min-h-[500px] w-full max-w-screen-lg border-stone-200 bg-white sm:mb-[calc(20vh)] sm:rounded-lg sm:border sm:shadow-lg".
*/
className?: string;
/**
* The default value to use for the editor.
* Defaults to defaultEditorContent.
*/
defaultValue?: JSONContent | string;
/**
* A list of extensions to use for the editor, in addition to the default inke extensions.
* Defaults to [].
*/
extensions?: Extensions;
/**
* Props to pass to the underlying Tiptap editor, in addition to the default inke editor props.
* Defaults to {}.
*/
editorProps?: EditorProps;
/**
* A callback function that is called whenever the editor is updated.
* Defaults to () => {}.
*/
// eslint-disable-next-line no-unused-vars
onUpdate?: (editor?: EditorClass) => void;
/**
* A callback function that is called whenever the editor is updated, but only after the defined debounce duration.
* Defaults to () => {}.
*/
// eslint-disable-next-line no-unused-vars
onDebouncedUpdate?: (
json: JSONContent,
text: string,
markdown: string,
editor: EditorClass
) => void;
/**
* The duration (in milliseconds) to debounce the onDebouncedUpdate callback.
* Defaults to 750.
*/
debounceDuration?: number;
/**
* The key to use for storing the editor's value in local storage.
* Defaults to "novel__content".
*/
storageKey?: string;
/**
* Disable local storage read/save.
* Defaults to false.
*/
disableLocalStorage?: boolean;
/**
* Enable editing.
* Defaults to true.
*/
editable?: boolean;
plan?: string;
}) {
const [content, setContent] = useLocalStorage(storageKey, defaultValue);
const [hydrated, setHydrated] = useState(false);
const [isLoadingOutside, setLoadingOutside] = useState(false);
const debouncedUpdates = useDebouncedCallback(async ({ editor }) => {
const json = editor.getJSON();
const text = editor.getText();
const markdown = editor.storage.markdown.getMarkdown();
onDebouncedUpdate(json, text, markdown, editor);
if (!disableLocalStorage) {
setContent(json);
}
}, debounceDuration);
const editor = useEditor({
extensions: [...defaultExtensions, ...extensions],
editorProps: {
...defaultEditorProps,
...editorProps,
},
editable: editable,
onUpdate: (e) => {
const selection = e.editor.state.selection;
const lastTwo = getPrevText(e.editor, {
chars: 2,
});
if (lastTwo === "??" && !isLoading) {
setLoadingOutside(true);
e.editor.commands.deleteRange({
from: selection.from - 2,
to: selection.from,
});
complete(
getPrevText(e.editor, {
chars: 5000,
})
);
va.track("Autocomplete Shortcut Used");
} else {
onUpdate(e.editor);
debouncedUpdates(e);
}
},
autofocus: false,
});
const { complete, completion, isLoading, stop } = useCompletion({
id: "ai-continue",
api: `${completionApi}/continue`,
body: { plan },
onFinish: (_prompt, completion) => {
editor?.commands.setTextSelection({
from: editor.state.selection.from - completion.length,
to: editor.state.selection.from,
});
},
onError: (err) => {
toast.error(err.message);
if (err.message === "You have reached your request limit for the day.") {
va.track("Rate Limit Reached");
}
},
});
const prev = useRef("");
// Insert chunks of the generated text
useEffect(() => {
const diff = completion.slice(prev.current.length);
prev.current = completion;
editor?.commands.insertContent(diff);
if (!isLoading) {
setLoadingOutside(false);
}
}, [isLoading, editor, completion]);
// Default: Hydrate the editor with the content from localStorage.
// If disableLocalStorage is true, hydrate the editor with the defaultValue.
useEffect(() => {
if (!editor || hydrated) return;
const value = disableLocalStorage ? defaultValue : content;
if (value) {
editor.commands.setContent(value);
setHydrated(true);
}
}, [editor, defaultValue, content, hydrated, disableLocalStorage]);
return (
<NovelContext.Provider
value={{
completionApi,
plan,
}}>
<div
onClick={() => {
editor?.chain().focus().run();
}}
className={className}>
{editor && (
<>
<EditorBubbleMenu editor={editor} />
<AIEditorBubble editor={editor} />
<AITranslateBubble editor={editor} />
</>
)}
{editor?.isActive("image") && <ImageResizer editor={editor} />}
<EditorContent editor={editor} />
{isLoadingOutside && isLoading && (
<div className="novel-fixed novel-bottom-3 novel-right-3">
<AIGeneratingLoading stop={stop} />
</div>
)}
</div>
</NovelContext.Provider>
);
}