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,17 @@
import { Magic } from "@/ui/icons";
import { PauseCircle } from "lucide-react";
export default function AIGeneratingLoading({ stop }: { stop: () => void }) {
return (
<div className="flex items-center justify-start novel-bg-white shadow-lg rounded-full px-3 py-2 w-16 h-10">
<Magic className="novel-w-7 novel-animate-pulse novel-text-purple-500" />
<span className="text-sm novel-animate-pulse novel-ml-1 novel-text-slate-500">
generating...
</span>
<PauseCircle
onClick={stop}
className="novel-h-5 hover:novel-text-stone-500 cursor-pointer novel-ml-6 novel-w-5 novel-text-stone-300"
/>
</div>
);
}

View File

@@ -0,0 +1,93 @@
import LoadingDots from "@/ui/icons/loading-dots";
import Magic from "@/ui/icons/magic";
import { Editor } from "@tiptap/core";
import { useCompletion } from "ai/react";
import { X, Clipboard, Replace } from "lucide-react";
import { useContext, useEffect, useState } from "react";
import { toast } from "sonner";
import va from "@vercel/analytics";
import { NovelContext } from "../../../provider";
type Props = {
editor: Editor;
};
const AIEditorBubble: React.FC<Props> = ({ editor }: Props) => {
const [isShow, setIsShow] = useState(false);
const { completionApi, plan } = useContext(NovelContext);
const { completion, setCompletion, isLoading, stop } = useCompletion({
id: "novel-edit",
api: `${completionApi}/edit`,
body: { plan },
onError: (err) => {
toast.error(err.message);
if (err.message === "You have reached your request limit for the day.") {
va.track("Rate Limit Reached");
}
},
});
useEffect(() => {
if (completion.length > 0) {
setIsShow(true);
}
}, [completion]);
const handleCopy = () => {
navigator.clipboard.writeText(completion);
};
const handleReplace = () => {
if (completion.length > 0) {
const { from, to } = editor.state.selection;
editor.commands.insertContent(completion, {
updateSelection: true,
});
}
};
return isShow || isLoading ? (
<div className="novel-fixed z-[1000] novel-bottom-3 novel-right-3 novel-p-3 novel-overflow-hidden novel-rounded novel-border novel-border-stone-200 novel-bg-white novel-shadow-xl novel-animate-in novel-fade-in novel-slide-in-from-bottom-1">
<div className="novel-w-64 novel-max-h-48 novel-overflow-y-auto">
<div className=" novel-flex novel-gap-2 novel-items-center novel-text-slate-500">
<Magic className="novel-h-5 novel-animate-pulse novel-w-5 novel-text-purple-500" />
{isLoading && (
<div className="novel-mr-auto novel-flex novel-items-center">
<LoadingDots color="#9e9e9e" />
</div>
)}
<div className="novel-flex novel-items-center novel-ml-auto gap-2">
<button>
<Replace
onClick={handleReplace}
className="novel-w-4 novel-h-4 novel-cursor-pointer hover:novel-text-slate-300 "
/>
</button>
<button>
<Clipboard
onClick={handleCopy}
className="novel-w-4 active:novel-text-green-500 novel-h-4 novel-cursor-pointer hover:novel-text-slate-300 "
/>
</button>
<X
onClick={() => {
setIsShow(false);
setCompletion("");
}}
className="novel-w-4 novel-h-4 novel-cursor-pointer hover:novel-text-slate-300 "
/>
</div>
</div>
{completion.length > 0 && (
<div className="novel-text-sm mt-2">{completion}</div>
)}
</div>
</div>
) : null;
};
export default AIEditorBubble;

View File

@@ -0,0 +1,166 @@
import { Editor } from "@tiptap/core";
import {
Beef,
Book,
CheckCheck,
ChevronDown,
Heading1,
LayoutPanelTop,
ListMinus,
ListPlus,
PartyPopper,
PauseCircle,
Pipette,
Repeat,
Scissors,
Wand,
} from "lucide-react";
import { FC, useContext, useEffect } from "react";
import { Command } from "cmdk";
import Magic from "@/ui/icons/magic";
import { useCompletion } from "ai/react";
import { NovelContext } from "../../../provider";
interface AISelectorProps {
editor: Editor;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
}
export const AISelector: FC<AISelectorProps> = ({
editor,
isOpen,
setIsOpen,
}) => {
const items = [
{
name: "Improve writing",
detail: "Improve writing",
icon: Wand,
},
{
name: "Fix spelling & grammar",
detail:
"Please correct spelling and grammar errors in the following text",
icon: CheckCheck,
},
{
name: "Make shorter",
detail: "Make shorter",
icon: ListMinus,
},
{
name: "Make longer",
detail: "Make longer",
icon: ListPlus,
},
{
name: "Writing suggestions",
detail: "Provide suggestions and improvements for the writing",
icon: Beef,
},
{
name: "Enhance vocabulary",
detail: "Suggest synonyms and expand vocabulary usage",
icon: Book,
},
{
name: "Generate titles",
detail: "Automatically generate compelling titles for the content",
icon: Heading1,
},
{
name: "Templates & structure",
detail:
"Offer templates and structure suggestions to improve the writing organization",
icon: LayoutPanelTop,
},
{
name: "Fix repetitive",
detail: "Identify and fix repetitive words or phrases in the content",
icon: Scissors,
},
{
name: "Adjust writing style",
detail:
"Suggest adjustments to writing style and tone based on the target audience",
icon: PartyPopper,
},
];
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (["ArrowUp", "ArrowDown", "Enter"].includes(e.key)) {
e.preventDefault();
}
};
if (isOpen) {
document.addEventListener("keydown", onKeyDown);
} else {
document.removeEventListener("keydown", onKeyDown);
}
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [isOpen]);
const { completionApi, plan } = useContext(NovelContext);
const { complete, isLoading, stop } = useCompletion({
id: "novel-edit",
api: `${completionApi}/edit`,
body: { plan },
});
return (
<div className="novel-relative novel-h-full">
<div className="novel-flex novel-h-full novel-items-center novel-gap-1 novel-text-sm novel-font-medium novel-text-purple-500 hover:novel-bg-stone-100 active:novel-bg-stone-200">
<button
className="novel-p-2 novel-flex novel-h-full novel-items-center novel-gap-1"
onClick={() => {
if (isLoading) {
stop();
}
setIsOpen(!isOpen);
}}>
<Magic className="novel-h-4 novel-w-4" />
<span className="novel-whitespace-nowrap">Ask AI</span>
{!isLoading ? (
<ChevronDown className="novel-h-4 novel-w-4" />
) : (
<PauseCircle
onClick={stop}
className="novel-h-4 hover:novel-text-stone-500 cursor-pointer novel-w-4 novel-text-stone-300"
/>
)}
</button>
</div>
{isOpen && (
<Command className="novel-fixed novel-top-full novel-z-[99999] novel-mt-1 novel-w-60 novel-overflow-hidden novel-rounded novel-border novel-border-stone-200 novel-bg-white novel-p-2 novel-shadow-xl novel-animate-in novel-fade-in novel-slide-in-from-top-1">
<Command.List>
{items.map((item, index) => (
<Command.Item
key={index}
onSelect={() => {
if (!isLoading) {
const { from, to } = editor.state.selection;
const text = editor.state.doc.textBetween(from, to, " ");
complete(`${item.detail}:\n ${text}`);
setIsOpen(false);
}
}}
className="novel-flex group novel-cursor-pointer novel-items-center novel-justify-between novel-rounded-sm novel-px-2 novel-py-1 novel-text-sm novel-text-gray-600 active:novel-bg-stone-200 aria-selected:novel-bg-stone-100">
<div className="novel-flex novel-items-center novel-space-x-2">
<item.icon className="novel-h-4 novel-w-4 novel-text-purple-500" />
<span>{item.name}</span>
</div>
{/* <CornerDownLeft className="novel-hidden novel-h-4 novel-w-4 group-hover:novel-block" /> */}
</Command.Item>
))}
</Command.List>
</Command>
)}
</div>
);
};

View File

@@ -0,0 +1,93 @@
import LoadingDots from "@/ui/icons/loading-dots";
import Magic from "@/ui/icons/magic";
import { Editor } from "@tiptap/core";
import { useCompletion } from "ai/react";
import { X, Clipboard, Replace } from "lucide-react";
import { useContext, useEffect, useState } from "react";
import { toast } from "sonner";
import va from "@vercel/analytics";
import { NovelContext } from "../../../provider";
type Props = {
editor: Editor;
};
const AITranslateBubble: React.FC<Props> = ({ editor }: Props) => {
const [isShow, setIsShow] = useState(false);
const { completionApi, plan } = useContext(NovelContext);
const { completion, setCompletion, isLoading, stop } = useCompletion({
id: "novel-translate",
api: `${completionApi}/translate`,
body: { plan },
onError: (err) => {
toast.error(err.message);
if (err.message === "You have reached your request limit for the day.") {
va.track("Rate Limit Reached");
}
},
});
useEffect(() => {
if (completion.length > 0) {
setIsShow(true);
}
}, [completion]);
const handleCopy = () => {
navigator.clipboard.writeText(completion);
};
const handleReplace = () => {
if (completion.length > 0) {
const { from, to } = editor.state.selection;
editor.commands.insertContent(completion, {
updateSelection: true,
});
}
};
return isShow || isLoading ? (
<div className="novel-fixed z-[1000] novel-bottom-3 novel-right-3 novel-p-3 novel-overflow-hidden novel-rounded novel-border novel-border-stone-200 novel-bg-white novel-shadow-xl novel-animate-in novel-fade-in novel-slide-in-from-bottom-1">
<div className="novel-w-64 novel-max-h-48 novel-overflow-y-auto">
<div className=" novel-flex novel-gap-2 novel-items-center novel-text-slate-500">
<Magic className="novel-h-5 novel-animate-pulse novel-w-5 novel-text-purple-500" />
{isLoading && (
<div className="novel-mr-auto novel-flex novel-items-center">
<LoadingDots color="#9e9e9e" />
</div>
)}
<div className="novel-flex novel-items-center novel-ml-auto gap-2">
<button>
<Replace
onClick={handleReplace}
className="novel-w-4 novel-h-4 novel-cursor-pointer hover:novel-text-slate-300 "
/>
</button>
<button>
<Clipboard
onClick={handleCopy}
className="novel-w-4 active:novel-text-green-500 novel-h-4 novel-cursor-pointer hover:novel-text-slate-300 "
/>
</button>
<X
onClick={() => {
setIsShow(false);
setCompletion("");
}}
className="novel-w-4 novel-h-4 novel-cursor-pointer hover:novel-text-slate-300 "
/>
</div>
</div>
{completion.length > 0 && (
<div className="novel-text-sm mt-2">{completion}</div>
)}
</div>
</div>
) : null;
};
export default AITranslateBubble;

View File

@@ -0,0 +1,129 @@
import { Editor } from "@tiptap/core";
import { Globe2, Languages, PauseCircle } from "lucide-react";
import { FC, useContext, useEffect } from "react";
import { Command } from "cmdk";
import { useCompletion } from "ai/react";
import { NovelContext } from "../../../provider";
interface TranslateSelectorProps {
editor: Editor;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
}
export const TranslateSelector: FC<TranslateSelectorProps> = ({
editor,
isOpen,
setIsOpen,
}) => {
const items = [
{
name: "English",
detail: "Translate into English",
},
{
name: "Chinese",
detail: "Translate into Chinese",
},
{
name: "Spanish",
detail: "Translate into Spanish",
},
{
name: "French",
detail: "Translate into French",
},
{
name: "German",
detail: "Translate into German",
},
{
name: "Japanese",
detail: "Translate into Japanese",
},
{
name: "Russian",
detail: "Translate into Russian",
},
{
name: "Korean",
detail: "Translate into Korean",
},
{
name: "Arabic",
detail: "Translate into Arabic",
},
{
name: "Portuguese",
detail: "Translate into Portuguese",
},
];
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (["ArrowUp", "ArrowDown", "Enter"].includes(e.key)) {
e.preventDefault();
}
};
if (isOpen) {
document.addEventListener("keydown", onKeyDown);
} else {
document.removeEventListener("keydown", onKeyDown);
}
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [isOpen]);
const { completionApi, plan } = useContext(NovelContext);
const { complete, isLoading, stop } = useCompletion({
id: "novel-translate",
api: `${completionApi}/translate`,
body: { plan },
});
return (
<div className="novel-relative novel-h-full">
<div className="novel-flex novel-h-full novel-items-center novel-text-sm novel-font-medium hover:novel-bg-stone-100 active:novel-bg-stone-200">
{isLoading ? (
<button className="p-2">
<PauseCircle
onClick={stop}
className="novel-h-5 hover:novel-text-stone-500 cursor-pointer novel-w-4 novel-text-stone-300"
/>
</button>
) : (
<button className="p-2">
<Languages
onClick={() => setIsOpen(!isOpen)}
className="novel-h-5 novel-text-stone-600 novel-w-4"
/>
</button>
)}
</div>
{isOpen && (
<Command className="novel-fixed novel-top-full novel-z-[99999] novel-mt-1 novel-w-28 novel-overflow-hidden novel-rounded novel-border novel-border-stone-200 novel-bg-white novel-p-2 novel-shadow-xl novel-animate-in novel-fade-in novel-slide-in-from-top-1">
<Command.List>
{items.map((item, index) => (
<Command.Item
key={index}
onSelect={() => {
if (!isLoading) {
const { from, to } = editor.state.selection;
const text = editor.state.doc.textBetween(from, to, " ");
complete(`${item.detail}:\n ${text}`);
setIsOpen(false);
}
}}
className="novel-flex novel-cursor-pointer novel-items-center novel-justify-between novel-rounded-sm novel-px-2 novel-py-1 novel-text-sm novel-text-gray-600 active:novel-bg-stone-200 aria-selected:novel-bg-stone-100">
<span>{item.name}</span>
</Command.Item>
))}
</Command.List>
</Command>
)}
</div>
);
};