tetrons 2.3.24 → 2.3.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/components/tetrons/ResizableImageComponent.tsx +64 -29
- package/dist/components/tetrons/ResizableImage.d.ts +1 -0
- package/dist/components/tetrons/ResizableImage.ts +2 -6
- package/dist/components/tetrons/ResizableImageComponent.d.ts +4 -0
- package/dist/components/tetrons/ResizableImageComponent.jsx +73 -0
- package/dist/components/tetrons/ResizableImageComponent.tsx +64 -29
- package/dist/components/tetrons/toolbar/AIGroup.tsx +209 -0
- package/dist/components/tetrons/toolbar/TetronsToolbar.tsx +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8449 -8809
- package/dist/index.mjs +8458 -8818
- package/dist/styles/styles/tetrons.css +192 -0
- package/dist/styles/tetrons.css +192 -0
- package/package.json +48 -40
- package/dist/app/page.jsx +0 -51
- package/dist/components/tetrons/toolbar/MiscGroup.d.ts +0 -7
- package/dist/components/tetrons/toolbar/MiscGroup.jsx +0 -55
- package/dist/components/tetrons/toolbar/TetronsToolbar.d.ts +0 -6
- package/dist/utils/checkGrammar.d.ts +0 -25
- package/dist/utils/checkGrammar.js +0 -17
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
import React, { useRef, useEffect } from "react";
|
|
2
|
-
import { NodeViewWrapper,
|
|
1
|
+
import React, { useRef, useEffect, useState } from "react";
|
|
2
|
+
import { NodeViewWrapper, ReactNodeViewProps } from "@tiptap/react";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
updateAttributes: (attrs: {
|
|
6
|
-
width?: number | null;
|
|
7
|
-
height?: number | null;
|
|
8
|
-
}) => void;
|
|
9
|
-
selected?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const ResizableImageComponent: React.FC<ResizableImageProps> = ({
|
|
4
|
+
const ResizableImageComponent: React.FC<ReactNodeViewProps> = ({
|
|
13
5
|
node,
|
|
14
6
|
updateAttributes,
|
|
15
7
|
selected,
|
|
@@ -21,22 +13,44 @@ const ResizableImageComponent: React.FC<ResizableImageProps> = ({
|
|
|
21
13
|
width?: number | null;
|
|
22
14
|
height?: number | null;
|
|
23
15
|
};
|
|
16
|
+
|
|
17
|
+
const defaultWidth = width ?? 300;
|
|
18
|
+
const defaultHeight = height ?? 200;
|
|
19
|
+
const aspectRatio = defaultHeight > 0 ? defaultWidth / defaultHeight : 4 / 3;
|
|
20
|
+
|
|
24
21
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
25
|
-
const
|
|
22
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
26
23
|
|
|
27
24
|
useEffect(() => {
|
|
28
|
-
const
|
|
29
|
-
|
|
25
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
26
|
+
if (!isResizing || !wrapperRef.current) return;
|
|
27
|
+
|
|
28
|
+
const rect = wrapperRef.current.getBoundingClientRect();
|
|
29
|
+
const newWidth = e.clientX - rect.left;
|
|
30
|
+
const newHeight = newWidth / aspectRatio;
|
|
31
|
+
|
|
32
|
+
if (newWidth > 50 && newHeight > 50) {
|
|
33
|
+
updateAttributes({
|
|
34
|
+
width: Math.round(newWidth),
|
|
35
|
+
height: Math.round(newHeight),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleMouseUp = () => {
|
|
41
|
+
setIsResizing(false);
|
|
42
|
+
};
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
44
|
+
if (isResizing) {
|
|
45
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
46
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
47
|
+
}
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
return () => {
|
|
50
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
51
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
52
|
+
};
|
|
53
|
+
}, [isResizing, updateAttributes, aspectRatio]);
|
|
40
54
|
|
|
41
55
|
return (
|
|
42
56
|
<NodeViewWrapper
|
|
@@ -46,30 +60,51 @@ const ResizableImageComponent: React.FC<ResizableImageProps> = ({
|
|
|
46
60
|
selected ? "ProseMirror-selectednode" : ""
|
|
47
61
|
}`}
|
|
48
62
|
style={{
|
|
49
|
-
|
|
50
|
-
overflow: "auto",
|
|
63
|
+
position: "relative",
|
|
51
64
|
border: "1px solid #ccc",
|
|
52
|
-
padding: 2,
|
|
53
65
|
display: "inline-block",
|
|
66
|
+
width: `${defaultWidth}px`,
|
|
67
|
+
height: `${defaultHeight}px`,
|
|
68
|
+
minWidth: "50px",
|
|
69
|
+
minHeight: "50px",
|
|
54
70
|
maxWidth: "100%",
|
|
71
|
+
userSelect: "none",
|
|
72
|
+
padding: 2,
|
|
55
73
|
}}
|
|
56
74
|
>
|
|
57
|
-
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
58
75
|
<img
|
|
59
|
-
ref={imgRef}
|
|
60
76
|
src={src}
|
|
61
77
|
alt={alt ?? ""}
|
|
62
78
|
title={title ?? ""}
|
|
63
79
|
loading="lazy"
|
|
64
80
|
style={{
|
|
65
|
-
width:
|
|
66
|
-
height:
|
|
81
|
+
width: "100%",
|
|
82
|
+
height: "100%",
|
|
83
|
+
objectFit: "contain",
|
|
67
84
|
display: "block",
|
|
68
85
|
userSelect: "none",
|
|
69
86
|
pointerEvents: "auto",
|
|
70
87
|
}}
|
|
71
88
|
draggable={false}
|
|
72
89
|
/>
|
|
90
|
+
|
|
91
|
+
<div
|
|
92
|
+
onMouseDown={(e) => {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
setIsResizing(true);
|
|
95
|
+
}}
|
|
96
|
+
style={{
|
|
97
|
+
position: "absolute",
|
|
98
|
+
width: "12px",
|
|
99
|
+
height: "12px",
|
|
100
|
+
right: 2,
|
|
101
|
+
bottom: 2,
|
|
102
|
+
background: "#ccc",
|
|
103
|
+
borderRadius: "2px",
|
|
104
|
+
cursor: "nwse-resize",
|
|
105
|
+
zIndex: 10,
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
73
108
|
</NodeViewWrapper>
|
|
74
109
|
);
|
|
75
110
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ResizableImage: import("@tiptap/react").Node<import("@tiptap/extension-image").ImageOptions, any>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React, { useRef, useEffect, useState } from "react";
|
|
2
|
+
import { NodeViewWrapper } from "@tiptap/react";
|
|
3
|
+
const ResizableImageComponent = ({ node, updateAttributes, selected, }) => {
|
|
4
|
+
const { src, alt, title, width, height } = node.attrs;
|
|
5
|
+
const defaultWidth = width !== null && width !== void 0 ? width : 300;
|
|
6
|
+
const defaultHeight = height !== null && height !== void 0 ? height : 200;
|
|
7
|
+
const aspectRatio = defaultHeight > 0 ? defaultWidth / defaultHeight : 4 / 3;
|
|
8
|
+
const wrapperRef = useRef(null);
|
|
9
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const handleMouseMove = (e) => {
|
|
12
|
+
if (!isResizing || !wrapperRef.current)
|
|
13
|
+
return;
|
|
14
|
+
const rect = wrapperRef.current.getBoundingClientRect();
|
|
15
|
+
const newWidth = e.clientX - rect.left;
|
|
16
|
+
const newHeight = newWidth / aspectRatio;
|
|
17
|
+
if (newWidth > 50 && newHeight > 50) {
|
|
18
|
+
updateAttributes({
|
|
19
|
+
width: Math.round(newWidth),
|
|
20
|
+
height: Math.round(newHeight),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const handleMouseUp = () => {
|
|
25
|
+
setIsResizing(false);
|
|
26
|
+
};
|
|
27
|
+
if (isResizing) {
|
|
28
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
29
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
30
|
+
}
|
|
31
|
+
return () => {
|
|
32
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
33
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
34
|
+
};
|
|
35
|
+
}, [isResizing, updateAttributes, aspectRatio]);
|
|
36
|
+
return (<NodeViewWrapper ref={wrapperRef} contentEditable={false} className={`resizable-image-wrapper ${selected ? "ProseMirror-selectednode" : ""}`} style={{
|
|
37
|
+
position: "relative",
|
|
38
|
+
border: "1px solid #ccc",
|
|
39
|
+
display: "inline-block",
|
|
40
|
+
width: `${defaultWidth}px`,
|
|
41
|
+
height: `${defaultHeight}px`,
|
|
42
|
+
minWidth: "50px",
|
|
43
|
+
minHeight: "50px",
|
|
44
|
+
maxWidth: "100%",
|
|
45
|
+
userSelect: "none",
|
|
46
|
+
padding: 2,
|
|
47
|
+
}}>
|
|
48
|
+
<img src={src} alt={alt !== null && alt !== void 0 ? alt : ""} title={title !== null && title !== void 0 ? title : ""} loading="lazy" style={{
|
|
49
|
+
width: "100%",
|
|
50
|
+
height: "100%",
|
|
51
|
+
objectFit: "contain",
|
|
52
|
+
display: "block",
|
|
53
|
+
userSelect: "none",
|
|
54
|
+
pointerEvents: "auto",
|
|
55
|
+
}} draggable={false}/>
|
|
56
|
+
|
|
57
|
+
<div onMouseDown={(e) => {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
setIsResizing(true);
|
|
60
|
+
}} style={{
|
|
61
|
+
position: "absolute",
|
|
62
|
+
width: "12px",
|
|
63
|
+
height: "12px",
|
|
64
|
+
right: 2,
|
|
65
|
+
bottom: 2,
|
|
66
|
+
background: "#ccc",
|
|
67
|
+
borderRadius: "2px",
|
|
68
|
+
cursor: "nwse-resize",
|
|
69
|
+
zIndex: 10,
|
|
70
|
+
}}/>
|
|
71
|
+
</NodeViewWrapper>);
|
|
72
|
+
};
|
|
73
|
+
export default ResizableImageComponent;
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
import React, { useRef, useEffect } from "react";
|
|
2
|
-
import { NodeViewWrapper,
|
|
1
|
+
import React, { useRef, useEffect, useState } from "react";
|
|
2
|
+
import { NodeViewWrapper, ReactNodeViewProps } from "@tiptap/react";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
updateAttributes: (attrs: {
|
|
6
|
-
width?: number | null;
|
|
7
|
-
height?: number | null;
|
|
8
|
-
}) => void;
|
|
9
|
-
selected?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const ResizableImageComponent: React.FC<ResizableImageProps> = ({
|
|
4
|
+
const ResizableImageComponent: React.FC<ReactNodeViewProps> = ({
|
|
13
5
|
node,
|
|
14
6
|
updateAttributes,
|
|
15
7
|
selected,
|
|
@@ -21,22 +13,44 @@ const ResizableImageComponent: React.FC<ResizableImageProps> = ({
|
|
|
21
13
|
width?: number | null;
|
|
22
14
|
height?: number | null;
|
|
23
15
|
};
|
|
16
|
+
|
|
17
|
+
const defaultWidth = width ?? 300;
|
|
18
|
+
const defaultHeight = height ?? 200;
|
|
19
|
+
const aspectRatio = defaultHeight > 0 ? defaultWidth / defaultHeight : 4 / 3;
|
|
20
|
+
|
|
24
21
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
25
|
-
const
|
|
22
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
26
23
|
|
|
27
24
|
useEffect(() => {
|
|
28
|
-
const
|
|
29
|
-
|
|
25
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
26
|
+
if (!isResizing || !wrapperRef.current) return;
|
|
27
|
+
|
|
28
|
+
const rect = wrapperRef.current.getBoundingClientRect();
|
|
29
|
+
const newWidth = e.clientX - rect.left;
|
|
30
|
+
const newHeight = newWidth / aspectRatio;
|
|
31
|
+
|
|
32
|
+
if (newWidth > 50 && newHeight > 50) {
|
|
33
|
+
updateAttributes({
|
|
34
|
+
width: Math.round(newWidth),
|
|
35
|
+
height: Math.round(newHeight),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleMouseUp = () => {
|
|
41
|
+
setIsResizing(false);
|
|
42
|
+
};
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
44
|
+
if (isResizing) {
|
|
45
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
46
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
47
|
+
}
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
return () => {
|
|
50
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
51
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
52
|
+
};
|
|
53
|
+
}, [isResizing, updateAttributes, aspectRatio]);
|
|
40
54
|
|
|
41
55
|
return (
|
|
42
56
|
<NodeViewWrapper
|
|
@@ -46,30 +60,51 @@ const ResizableImageComponent: React.FC<ResizableImageProps> = ({
|
|
|
46
60
|
selected ? "ProseMirror-selectednode" : ""
|
|
47
61
|
}`}
|
|
48
62
|
style={{
|
|
49
|
-
|
|
50
|
-
overflow: "auto",
|
|
63
|
+
position: "relative",
|
|
51
64
|
border: "1px solid #ccc",
|
|
52
|
-
padding: 2,
|
|
53
65
|
display: "inline-block",
|
|
66
|
+
width: `${defaultWidth}px`,
|
|
67
|
+
height: `${defaultHeight}px`,
|
|
68
|
+
minWidth: "50px",
|
|
69
|
+
minHeight: "50px",
|
|
54
70
|
maxWidth: "100%",
|
|
71
|
+
userSelect: "none",
|
|
72
|
+
padding: 2,
|
|
55
73
|
}}
|
|
56
74
|
>
|
|
57
|
-
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
58
75
|
<img
|
|
59
|
-
ref={imgRef}
|
|
60
76
|
src={src}
|
|
61
77
|
alt={alt ?? ""}
|
|
62
78
|
title={title ?? ""}
|
|
63
79
|
loading="lazy"
|
|
64
80
|
style={{
|
|
65
|
-
width:
|
|
66
|
-
height:
|
|
81
|
+
width: "100%",
|
|
82
|
+
height: "100%",
|
|
83
|
+
objectFit: "contain",
|
|
67
84
|
display: "block",
|
|
68
85
|
userSelect: "none",
|
|
69
86
|
pointerEvents: "auto",
|
|
70
87
|
}}
|
|
71
88
|
draggable={false}
|
|
72
89
|
/>
|
|
90
|
+
|
|
91
|
+
<div
|
|
92
|
+
onMouseDown={(e) => {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
setIsResizing(true);
|
|
95
|
+
}}
|
|
96
|
+
style={{
|
|
97
|
+
position: "absolute",
|
|
98
|
+
width: "12px",
|
|
99
|
+
height: "12px",
|
|
100
|
+
right: 2,
|
|
101
|
+
bottom: 2,
|
|
102
|
+
background: "#ccc",
|
|
103
|
+
borderRadius: "2px",
|
|
104
|
+
cursor: "nwse-resize",
|
|
105
|
+
zIndex: 10,
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
73
108
|
</NodeViewWrapper>
|
|
74
109
|
);
|
|
75
110
|
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef } from "react";
|
|
4
|
+
import { Editor } from "@tiptap/react";
|
|
5
|
+
import { FaMicrophone, FaStop } from "react-icons/fa";
|
|
6
|
+
import { Waveform } from "@uiball/loaders";
|
|
7
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
8
|
+
|
|
9
|
+
export default function AiGroup({ editor }: { editor: Editor }) {
|
|
10
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
11
|
+
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
|
|
12
|
+
const [isTranscribing, setIsTranscribing] = useState(false);
|
|
13
|
+
const [transcriptionError, setTranscriptionError] = useState("");
|
|
14
|
+
|
|
15
|
+
const [showPromptInput, setShowPromptInput] = useState(false);
|
|
16
|
+
const [prompt, setPrompt] = useState("");
|
|
17
|
+
const [isLoadingAI, setIsLoadingAI] = useState(false);
|
|
18
|
+
const [aiError, setAiError] = useState("");
|
|
19
|
+
|
|
20
|
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
|
21
|
+
const chunksRef = useRef<BlobPart[]>([]);
|
|
22
|
+
|
|
23
|
+
const startRecording = async () => {
|
|
24
|
+
setTranscriptionError("");
|
|
25
|
+
setAudioBlob(null);
|
|
26
|
+
|
|
27
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
28
|
+
const mediaRecorder = new MediaRecorder(stream);
|
|
29
|
+
mediaRecorderRef.current = mediaRecorder;
|
|
30
|
+
chunksRef.current = [];
|
|
31
|
+
|
|
32
|
+
mediaRecorder.ondataavailable = (e) => {
|
|
33
|
+
if (e.data.size > 0) chunksRef.current.push(e.data);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
mediaRecorder.onstop = () => {
|
|
37
|
+
const blob = new Blob(chunksRef.current, { type: "audio/webm" });
|
|
38
|
+
setAudioBlob(blob);
|
|
39
|
+
transcribeAudio(blob);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
mediaRecorder.start();
|
|
43
|
+
setIsRecording(true);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const stopRecording = () => {
|
|
47
|
+
mediaRecorderRef.current?.stop();
|
|
48
|
+
setIsRecording(false);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const transcribeAudio = async (blob: Blob) => {
|
|
52
|
+
setIsTranscribing(true);
|
|
53
|
+
setTranscriptionError("");
|
|
54
|
+
|
|
55
|
+
const formData = new FormData();
|
|
56
|
+
formData.append("file", blob, "voice.webm");
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch("/api/transcribe", {
|
|
60
|
+
method: "POST",
|
|
61
|
+
body: formData,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
if (!res.ok || !data.transcript) {
|
|
66
|
+
throw new Error(data.error || "Failed to transcribe");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
editor.commands.insertContent(data.transcript);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
console.error(e);
|
|
72
|
+
setTranscriptionError("Transcription failed. Please try again.");
|
|
73
|
+
} finally {
|
|
74
|
+
setIsTranscribing(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handleAiClick = () => {
|
|
79
|
+
setShowPromptInput(true);
|
|
80
|
+
setPrompt("");
|
|
81
|
+
setAiError("");
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handlePromptSubmit = async () => {
|
|
85
|
+
if (!prompt.trim()) return;
|
|
86
|
+
setIsLoadingAI(true);
|
|
87
|
+
setAiError("");
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch("/api/ai-action", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify({ content: prompt }),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const data = await res.json();
|
|
97
|
+
|
|
98
|
+
if (!res.ok || !data.response) {
|
|
99
|
+
throw new Error(data.error || "AI failed to generate content");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
editor.commands.insertContent(data.response);
|
|
103
|
+
setShowPromptInput(false);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error(e);
|
|
106
|
+
setAiError("Failed to generate content. Try again.");
|
|
107
|
+
} finally {
|
|
108
|
+
setIsLoadingAI(false);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="group relative space-y-3">
|
|
114
|
+
<div className="flex gap-2 items-center">
|
|
115
|
+
{!isRecording ? (
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={startRecording}
|
|
119
|
+
className="icon-btn"
|
|
120
|
+
title="Start Voice Input"
|
|
121
|
+
>
|
|
122
|
+
<FaMicrophone size={18} />
|
|
123
|
+
</button>
|
|
124
|
+
) : (
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
onClick={stopRecording}
|
|
128
|
+
className="icon-btn stop-btn"
|
|
129
|
+
title="Stop Recording"
|
|
130
|
+
>
|
|
131
|
+
<FaStop size={18} />
|
|
132
|
+
</button>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={handleAiClick}
|
|
138
|
+
className="ai-button"
|
|
139
|
+
title="AI Assist"
|
|
140
|
+
>
|
|
141
|
+
AI
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{isRecording && (
|
|
146
|
+
<div className="flex flex-col items-center">
|
|
147
|
+
<Waveform size={30} lineWeight={3.5} speed={1} color="#4F46E5" />
|
|
148
|
+
<p className="text-sm mt-1 text-gray-600">Recording...</p>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{isTranscribing && (
|
|
153
|
+
<p className="text-sm text-gray-500">Transcribing...</p>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{transcriptionError && (
|
|
157
|
+
<p className="text-sm text-red-600">{transcriptionError}</p>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{audioBlob && (
|
|
161
|
+
<div className="mt-2">
|
|
162
|
+
<audio controls src={URL.createObjectURL(audioBlob)} />
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
<AnimatePresence>
|
|
167
|
+
{showPromptInput && (
|
|
168
|
+
<motion.div
|
|
169
|
+
className="ai-modal-backdrop"
|
|
170
|
+
initial={{ opacity: 0 }}
|
|
171
|
+
animate={{ opacity: 1 }}
|
|
172
|
+
exit={{ opacity: 0 }}
|
|
173
|
+
>
|
|
174
|
+
<motion.div
|
|
175
|
+
className="ai-modal-content"
|
|
176
|
+
initial={{ scale: 0.9, opacity: 0 }}
|
|
177
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
178
|
+
exit={{ scale: 0.9, opacity: 0 }}
|
|
179
|
+
>
|
|
180
|
+
<h2 className="ai-modal-title">AI Prompt</h2>
|
|
181
|
+
<textarea
|
|
182
|
+
className="ai-modal-textarea"
|
|
183
|
+
value={prompt}
|
|
184
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
185
|
+
placeholder="Enter your prompt here..."
|
|
186
|
+
/>
|
|
187
|
+
{aiError && <p className="ai-modal-error">{aiError}</p>}
|
|
188
|
+
<div className="ai-modal-actions">
|
|
189
|
+
<button
|
|
190
|
+
onClick={() => setShowPromptInput(false)}
|
|
191
|
+
className="ai-cancel-btn"
|
|
192
|
+
>
|
|
193
|
+
Cancel
|
|
194
|
+
</button>
|
|
195
|
+
<button
|
|
196
|
+
onClick={handlePromptSubmit}
|
|
197
|
+
disabled={isLoadingAI}
|
|
198
|
+
className="ai-submit-btn"
|
|
199
|
+
>
|
|
200
|
+
{isLoadingAI ? "Generating..." : "Submit"}
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
</motion.div>
|
|
204
|
+
</motion.div>
|
|
205
|
+
)}
|
|
206
|
+
</AnimatePresence>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -10,6 +10,7 @@ import InsertGroup from "./InsertGroup";
|
|
|
10
10
|
import ListAlignGroup from "./ListAlignGroup";
|
|
11
11
|
import MiscGroup from "./MiscGroup";
|
|
12
12
|
import FileGroup from "./FileGroup";
|
|
13
|
+
import AiGroup from "./AIGroup";
|
|
13
14
|
|
|
14
15
|
export default function TetronsToolbar({
|
|
15
16
|
editor,
|
|
@@ -65,7 +66,12 @@ export default function TetronsToolbar({
|
|
|
65
66
|
<ActionGroup editor={editor} />
|
|
66
67
|
</>
|
|
67
68
|
)}
|
|
68
|
-
{version === "platinum" &&
|
|
69
|
+
{version === "platinum" && (
|
|
70
|
+
<>
|
|
71
|
+
<MiscGroup editor={editor} />
|
|
72
|
+
<AiGroup editor={editor} />
|
|
73
|
+
</>
|
|
74
|
+
)}
|
|
69
75
|
</div>
|
|
70
76
|
);
|
|
71
77
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import EditorContent from "./components/tetrons/EditorContent";
|
|
2
2
|
export declare function initializeTetrons(apiKey: string): Promise<void>;
|
|
3
|
-
export declare function getTetronsVersion(): "" | "
|
|
3
|
+
export declare function getTetronsVersion(): "" | "free" | "pro" | "premium" | "platinum";
|
|
4
4
|
export declare function isApiKeyValid(): boolean;
|
|
5
5
|
export { EditorContent };
|
|
6
6
|
export default EditorContent;
|