aboutsummaryrefslogtreecommitdiff
path: root/apps/web/components/editor/generative/ai-selector.tsx
blob: 62d381a10751236741475742d8b8d9e90a71ae59 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"use client";

import { Command, CommandInput } from "../ui/command";

import { useCompletion } from "ai/react";
import { ArrowUp } from "lucide-react";
import { useEditor } from "novel";
import { addAIHighlight } from "novel/extensions";
import { useState } from "react";
import Markdown from "react-markdown";
import { toast } from "sonner";
import { Button } from "../ui/button";
import CrazySpinner from "../ui/icons/crazy-spinner";
import Magic from "../ui/icons/magic";
import { ScrollArea } from "../ui/scroll-area";
import AICompletionCommands from "./ai-completion-command";
import AISelectorCommands from "./ai-selector-commands";
//TODO: I think it makes more sense to create a custom Tiptap extension for this functionality https://tiptap.dev/docs/editor/ai/introduction

interface AISelectorProps {
	open: boolean;
	onOpenChange: (open: boolean) => void;
}

export function AISelector({ onOpenChange }: AISelectorProps) {
	const { editor } = useEditor();
	const [inputValue, setInputValue] = useState("");

	const { completion, complete, isLoading } = useCompletion({
		// id: "novel",
		api: "/api/generate",
		onResponse: (response) => {
			if (response.status === 429) {
				toast.error("You have reached your request limit for the day.");
				return;
			}
		},
		onError: (e) => {
			toast.error(e.message);
		},
	});

	const hasCompletion = completion.length > 0;

	return (
		<Command className="w-[350px]">
			{hasCompletion && (
				<div className="flex max-h-[400px]">
					<ScrollArea>
						<div className="prose p-2 px-4 prose-sm">
							<Markdown>{completion}</Markdown>
						</div>
					</ScrollArea>
				</div>
			)}

			{isLoading && (
				<div className="flex h-12 w-full items-center px-4 text-sm font-medium text-muted-foreground text-purple-500">
					<Magic className="mr-2 h-4 w-4 shrink-0  " />
					AI is thinking
					<div className="ml-2 mt-1">
						<CrazySpinner />
					</div>
				</div>
			)}
			{!isLoading && (
				<>
					<div className="relative">
						<CommandInput
							value={inputValue}
							onValueChange={setInputValue}
							autoFocus
							placeholder={
								hasCompletion
									? "Tell AI what to do next"
									: "Ask AI to edit or generate..."
							}
							onFocus={() => addAIHighlight(editor!)}
						/>
						<Button
							size="icon"
							className="absolute right-2 top-1/2 h-6 w-6 -translate-y-1/2 rounded-full bg-purple-500 hover:bg-purple-900"
							onClick={() => {
								if (completion)
									return complete(completion, {
										body: { option: "zap", command: inputValue },
									}).then(() => setInputValue(""));

								const slice = editor?.state.selection.content();
								const text = editor?.storage.markdown.serializer.serialize(
									slice?.content,
								);

								complete(text, {
									body: { option: "zap", command: inputValue },
								}).then(() => setInputValue(""));
							}}
						>
							<ArrowUp className="h-4 w-4" />
						</Button>
					</div>
					{hasCompletion ? (
						<AICompletionCommands
							onDiscard={() => {
								editor?.chain().unsetHighlight().focus().run();
								onOpenChange(false);
							}}
							completion={completion}
						/>
					) : (
						<AISelectorCommands
							onSelect={(value, option) =>
								complete(value, { body: { option } })
							}
						/>
					)}
				</>
			)}
		</Command>
	);
}