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

feat: suppot remote image url

This commit is contained in:
songjunxi 2023-10-24 11:25:48 +08:00
parent 116e116332
commit 2b456637e9
8 changed files with 160 additions and 45 deletions

View File

@ -1,3 +1,5 @@
# Inke
<a href="https://www.npmjs.org/package/inkejs" target='_blank'>
<img src="https://img.shields.io/npm/v/inkejs">
</a>
@ -11,8 +13,6 @@
<img src="https://badgen.net/https/inke.app/api/status" alt="status"/>
</a>
# Inke
Inke is a Notion-style WYSIWYG editor with AI-powered autocompletions.
See live demo: [inke-web](https://inke.app)
@ -55,7 +55,7 @@ export default function App() {
The `Editor` is a React component that takes in the following props:
| Prop | Type | Description | Default |
| --------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| --------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `completionApi` | `string` | The API route to use for the OpenAI completion API. | `/api/generate` |
| `className` | `string` | Editor container classname. | `"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"` |
| `defaultValue` | `JSONContent` or `string` | The default value to use for the editor. | [`defaultEditorContent`](https://github.com/yesmore/inke/blob/main/packages/core/src/ui/editor/default-content.tsx) |

View File

@ -0,0 +1,95 @@
# Inke
<a href="https://www.npmjs.org/package/inkejs" target='_blank'>
<img src="https://img.shields.io/npm/v/inkejs">
</a>
<a href="https://npmcharts.com/compare/inkejs?minimal=true" target='_blank'>
<img src="https://img.shields.io/npm/dt/inkejs.svg">
</a>
<a href="https://github.com/yesmore/inke/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/yesmore/inke?label=license&logo=github&color=f80&logoColor=fff" alt="License" />
</a>
<a href="https://inke.app">
<img src="https://badgen.net/https/inke.app/api/status" alt="status"/>
</a>
Inke is a Notion-style WYSIWYG editor with AI-powered autocompletions.
See live demo: [inke-web](https://inke.app)
<img alt="Inke is a Notion-style WYSIWYG editor with AI-powered autocompletions." src="https://inke.app/desktop.png">
# Install Inke
```bash
npm install inkejs
```
## Setting Up Locally
To set up Inke locally, you'll need to clone the repository and set up the following environment variables:
- `OPENAI_API_KEY`  your OpenAI API key (you can get one [here](https://platform.openai.com/account/api-keys))
- `BLOB_READ_WRITE_TOKEN`  your Vercel Blob read/write token (currently [still in beta](https://vercel.com/docs/storage/vercel-blob/quickstart#quickstart), but feel free to [sign up on this form](https://vercel.fyi/blob-beta) for access)
If you've deployed this to Vercel, you can also use [`vc env pull`](https://vercel.com/docs/cli/env#exporting-development-environment-variables) to pull the environment variables from your Vercel project.
To run the app locally, you can run the following commands:
```bash
pnpm i
pnpm build
pnpm dev
```
Then, you can use it in your code like this:
```jsx
import { Editor } from "inkejs";
export default function App() {
return <Editor />;
}
```
The `Editor` is a React component that takes in the following props:
| Prop | Type | Description | Default |
| --------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `completionApi` | `string` | The API route to use for the OpenAI completion API. | `/api/generate` |
| `className` | `string` | Editor container classname. | `"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"` |
| `defaultValue` | `JSONContent` or `string` | The default value to use for the editor. | [`defaultEditorContent`](https://github.com/yesmore/inke/blob/main/packages/core/src/ui/editor/default-content.tsx) |
| `extensions` | `Extension[]` | A list of extensions to use for the editor, in addition to the [default Novel extensions](https://github.com/yesmore/inke/blob/main/packages/core/src/ui/editor/extensions/index.tsx). | `[]` |
| `editorProps` | `EditorProps` | Props to pass to the underlying Tiptap editor, in addition to the [default Novel editor props](https://github.com/yesmore/inke/blob/main/packages/core/src/ui/editor/props.ts). | `{}` |
| `onUpdate` | `(editor?: Editor) => void` | A callback function that is called whenever the editor is updated. | `() => {}` |
| `onDebouncedUpdate` | `(editor?: Editor) => void` | A callback function that is called whenever the editor is updated, but only after the defined debounce duration. | `() => {}` |
| `debounceDuration` | `number` | The duration (in milliseconds) to debounce the `onDebouncedUpdate` callback. | `750` |
| `storageKey` | `string` | The key to use for storing the editor's value in local storage. | `novel__content` |
| `disableLocalStorage` | `boolean` | Enabling this option will prevent read/write content from/to local storage. | `false` |
> **Note**: Make sure to define an API endpoint that matches the `completionApi` prop (default is `/api/generate`). This is needed for the AI autocompletions to work. Here's an example: https://github.com/yesmore/inke/blob/main/apps/web/app/api/generate/route.ts
## Deploy Your Own
You can deploy your own version of Novel to Vercel with one click:
[![Deploy with Vercel](https://vercel.com/button)]()
## Tech Stack
Inke is built on the following stack:
- [Next.js](https://nextjs.org/)  framework
- [Tiptap](https://tiptap.dev/)  text editor
- [OpenAI](https://openai.com/) - AI completions
- [Vercel AI SDK](https://sdk.vercel.ai/docs) AI library
- [Vercel](https://vercel.com)  deployments
- [TailwindCSS](https://tailwindcss.com/) styles
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=yesmore/inke&type=Date)](https://star-history.com/#yesmore/inke&Date)
## License
[Apache-2.0](./LICENSE) © [yesmore](https://github.com/yesmore)

View File

@ -1,7 +1,16 @@
{
"name": "inke",
"version": "0.1.4",
"description": "Notion-style WYSIWYG editor with AI-powered autocompletions",
"name": "inkejs",
"version": "0.0.5",
"author": "yesmore",
"description": "A Notion-style WYSIWYG editor with AI-powered autocompletions. Built with Tiptap, OpenAI, and Vercel AI SDK.",
"keywords": [
"editor",
"markdown",
"openai",
"ai",
"nextjs",
"react"
],
"license": "Apache-2.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
@ -85,7 +94,6 @@
"tsup": "^7.2.0",
"typescript": "^4.9.4"
},
"author": "Steven Tey <stevensteel97@gmail.com>",
"homepage": "https://inke.app",
"repository": {
"type": "git",
@ -93,13 +101,5 @@
},
"bugs": {
"url": "https://github.com/yesmore/inke/issues"
},
"keywords": [
"ai",
"inke",
"editor",
"markdown",
"nextjs",
"react"
]
}
}

View File

@ -24,3 +24,11 @@ export function getUrlFromString(str: string) {
return null;
}
}
export function isImageLink(link: string): boolean {
if (!isValidUrl(link)) return false;
const imageExtensions = [".jpg", ".jpeg", ".png", "webp", ".gif", ".bmp"];
const fileExtension = link.substring(link.lastIndexOf(".")).toLowerCase();
return imageExtensions.includes(fileExtension);
}

View File

@ -10,8 +10,6 @@ import {
ListPlus,
PartyPopper,
PauseCircle,
Pipette,
Repeat,
Scissors,
Wand,
} from "lucide-react";

View File

@ -28,8 +28,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
className="novel-flex novel-h-full novel-items-center novel-space-x-2 novel-px-3 novel-py-1.5 novel-text-sm novel-font-medium novel-text-stone-600 hover:novel-bg-stone-100 active:novel-bg-stone-200"
onClick={() => {
setIsOpen(!isOpen);
}}
>
}}>
<p className="novel-text-base"></p>
<p
className={cn(
@ -37,8 +36,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
{
"novel-text-blue-500": editor.isActive("link"),
}
)}
>
)}>
Link
</p>
</button>
@ -51,8 +49,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
url && editor.chain().focus().setLink({ href: url }).run();
setIsOpen(false);
}}
className="novel-fixed novel-top-full novel-z-[99999] novel-mt-1 novel-flex novel-w-60 novel-overflow-hidden novel-rounded novel-border novel-border-stone-200 novel-bg-white novel-p-1 novel-shadow-xl novel-animate-in novel-fade-in novel-slide-in-from-top-1"
>
className="novel-fixed novel-top-full novel-z-[99999] novel-mt-1 novel-flex novel-w-60 novel-overflow-hidden novel-rounded novel-border novel-border-stone-200 novel-bg-white novel-p-1 novel-shadow-xl novel-animate-in novel-fade-in novel-slide-in-from-top-1">
<input
ref={inputRef}
type="text"
@ -67,8 +64,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
onClick={() => {
editor.chain().focus().unsetLink().run();
setIsOpen(false);
}}
>
}}>
<Trash className="novel-h-4 novel-w-4" />
</button>
) : (

View File

@ -26,6 +26,7 @@ import {
CheckSquare,
Table2,
PauseCircle,
FileImage,
} from "lucide-react";
import { LoadingCircle } from "@/ui/icons";
import { toast } from "sonner";
@ -35,6 +36,7 @@ import { getPrevText } from "@/lib/editor";
import { startImageUpload } from "@/ui/editor/plugins/upload-images";
import { NovelContext } from "../provider";
import { Youtube } from "lucide-react";
import { isImageLink } from "@/lib/utils";
interface CommandItemProps {
title: string;
@ -211,10 +213,10 @@ const getSuggestionItems = ({
},
},
{
title: "Image",
title: "Local image",
description: "Upload an image from your computer.",
searchTerms: ["photo", "picture", "media"],
icon: <ImageIcon size={18} />,
searchTerms: ["photo", "picture", "media", "img"],
icon: <FileImage size={18} />,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).run();
// upload image
@ -231,6 +233,25 @@ const getSuggestionItems = ({
input.click();
},
},
{
title: "Remote image",
description: "Render an image from url.",
searchTerms: ["photo", "picture", "media", "img"],
icon: <ImageIcon size={18} />,
command: ({ editor, range }: CommandProps) => {
const url = prompt("Enter image url");
if (url && isImageLink(url)) {
editor
.chain()
.focus()
.deleteRange(range)
.setImage({
src: url,
})
.run();
}
},
},
{
title: "Youtube video",
description: "Play the Youtube video you filled out.",

View File

@ -2,8 +2,6 @@ import { BlobResult } from "@vercel/blob";
import { toast } from "sonner";
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
import { NovelContext } from "../provider";
import { useContext } from "react";
const uploadKey = new PluginKey("upload-image");
@ -26,7 +24,7 @@ const UploadImagesPlugin = () =>
const image = document.createElement("img");
image.setAttribute(
"class",
"opacity-40 rounded-lg border border-stone-200"
"opacity-40 rounded-md border border-stone-200"
);
image.src = src;
placeholder.appendChild(image);
@ -62,8 +60,8 @@ export function startImageUpload(file: File, view: EditorView, pos: number) {
if (!file.type.includes("image/")) {
toast.error("File type not supported.");
return;
} else if (file.size / 1024 / 1024 > 5) {
toast.error(`File size too big (max ${15}MB).`);
} else if (file.size / 1024 / 1024 > 1) {
toast.error(`File size too big (max ${1}MB).`);
return;
}
@ -111,7 +109,6 @@ export function startImageUpload(file: File, view: EditorView, pos: number) {
}
export const handleImageUpload = (file: File) => {
// upload to Vercel Blob
return new Promise((resolve) => {
toast.promise(
fetch("/api/upload", {
@ -125,6 +122,7 @@ export const handleImageUpload = (file: File) => {
// Successfully uploaded image
if (res.status === 200) {
const { url } = (await res.json()) as BlobResult;
// preload the image
let image = new Image();
image.src = url;
@ -134,7 +132,6 @@ export const handleImageUpload = (file: File) => {
// No blob store configured
} else if (res.status === 401) {
resolve(file);
throw new Error(
"`BLOB_READ_WRITE_TOKEN` environment variable not found, reading image locally instead."
);