paper-ai-release-24-07-21/components/QuillEditor.tsx

313 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useEffect, useRef } from "react";
import Quill from "quill";
import "quill/dist/quill.snow.css";
import { useLocalStorage } from "react-use";
import Link from "next/link";
// 一些工具函数导入
import getArxivPapers from "./GetArxiv";
import getSemanticPapers from "./GetSemantic";
import { getTopicFromAI, sendMessageToOpenAI, getFromAI } from "./chatAI";
import {
getTextBeforeCursor,
convertToSuperscript,
removeSpecialCharacters,
formatTextInEditor,
} from "@/utils/others/quillutils";
import ReferenceList from "./ReferenceList";
//redux
import { useAppDispatch, useAppSelector } from "@/app/store";
//类型声明
import { Reference } from "@/utils/global";
const toolbarOptions = [
["bold", "italic", "underline", "strike"], // 加粗、斜体、下划线和删除线
["blockquote", "code-block"], // 引用和代码块
[{ header: 1 }, { header: 2 }], // 标题
[{ list: "ordered" }, { list: "bullet" }], // 列表
[{ script: "sub" }, { script: "super" }], // 上标/下标
[{ indent: "-1" }, { indent: "+1" }], // 缩进
[{ direction: "rtl" }], // 文字方向
[{ size: ["small", false, "large", "huge"] }], // 字体大小
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }], // 字体颜色和背景色
[{ font: [] }], // 字体
[{ align: [] }], // 对齐方式
["clean"], // 清除格式按钮
];
const QEditor = () => {
//读取redux中的API key
const apiKey = useAppSelector((state: any) => state.auth.apiKey);
const [quill, setQuill] = useState(null);
//询问ai用户输入
const [userInput, setUserInput] = useState("你好");
//quill编辑器鼠标位置
const [cursorPosition, setCursorPosition] = useState(null);
// 初始化 Quill 编辑器
const isMounted = useRef(false);
const editor = useRef(null);
// 选择论文来源
const [selectedSource, setSelectedSource] = useLocalStorage(
"semanticScholar",
"semanticScholar"
); // 默认选项
//选择语言模型
const [selectedModel, setSelectedModel] = useLocalStorage("gpt3.5", "gpt3.5"); // 默认选项
//更新参考文献的部分
const [references, setReferences] = useLocalStorage<Reference[]>(
"referencesKey",
undefined
);
const addReference = (newReference: Reference) => {
setReferences([...references, newReference]);
};
const removeReference = (index: number) => {
setReferences(references.filter((_, i) => i !== index));
};
useEffect(() => {
if (!isMounted.current) {
editor.current = new Quill("#editor", {
modules: {
toolbar: toolbarOptions,
},
theme: "snow",
});
// 检查 localStorage 中是否有保存的内容
const savedContent = localStorage.getItem("quillContent");
if (savedContent) {
// 设置编辑器的内容
editor.current.root.innerHTML = savedContent;
}
isMounted.current = true;
setQuill(editor.current);
// 监听selection-change事件
editor.current.on("selection-change", function (range) {
if (range) {
// console.log('User has made a new selection', range);
setCursorPosition(range.index); // 更新光标位置
} else {
console.log("No selection or cursor in the editor.");
}
});
}
}, []);
useEffect(() => {
if (quill) {
// 设置监听器以处理内容变化
quill.on("text-change", function (delta, oldDelta, source) {
if (source === "user") {
// 获取编辑器内容
const content = quill.root.innerHTML; // 或 quill.getText(),或 quill.getContents()
// 保存到 localStorage
localStorage.setItem("quillContent", content);
setTimeout(() => {
convertToSuperscript(quill);
}, 0); // 延迟 0 毫秒,即将函数放入事件队列的下一个循环中执行,不然就会因为在改变文字触发整个函数时修改文本内容造成无法找到光标位置
}
});
}
}, [quill]);
// 处理用户输入变化
const handleInputChange = (event) => {
setUserInput(event.target.value);
};
// 处理AI写作
const handleAIWrite = async () => {
quill.setSelection(cursorPosition, 0); // 将光标移动到原来的位置
const prompt = "请帮助用户完成论文写作";
await sendMessageToOpenAI(userInput, quill, selectedModel, apiKey, prompt);
};
// 处理paper2AI
async function paper2AI(topic: string) {
quill.setSelection(cursorPosition, 0); // 将光标移动到原来的位置
try {
if (!topic) {
//使用ai提取当前要请求的论文主题
const prompt =
"As a topic extraction assistant, you can help me extract the current discussion of the paper topic, I will enter the content of the paper, you extract the paper topic , no more than two, Hyphenated query terms yield no matches (replace it with space to find matches) return format is: topic1 topic2";
const userMessage = getTextBeforeCursor(quill, 2000);
topic = await getTopicFromAI(userMessage, prompt, apiKey);
console.log("topic in AI before removeSpecialCharacters", topic);
topic = removeSpecialCharacters(topic);
topic = topic.split(" ").slice(0, 2).join(" ");
//如果超过十个字符就截断
if (topic.length > 10) {
topic = topic.slice(0, 10);
}
}
console.log("topic in AI", topic);
let rawData, dataString;
if (selectedSource === "arxiv") {
rawData = await getArxivPapers(topic);
console.log("arxiv rawdata:", rawData)
// 将 rawData 转换为引用数组
const newReferences = rawData.map((entry) => ({
url: entry.id,
title: entry.title,
year: entry.published,
author: entry.author?.slice(0, 3).join(", "),
}));
// 更新引用列表状态
setReferences((prevReferences) => [
...prevReferences,
...newReferences,
]);
dataString = rawData
.map((entry) => {
return `ID: ${entry.id}\nTime: ${entry.published}\nTitle: ${entry.title}\nSummary: ${entry.summary}\n\n`;
})
.join("");
} else if (selectedSource === "semanticScholar") {
rawData = await getSemanticPapers(topic, "2015-2023");
// 将 rawData 转换为引用数组
const newReferences = rawData.map((entry) => ({
url: entry.url,
title: entry.title,
year: entry.year,
author: entry.authors?.slice(0, 3).join(", "),
venue: entry.venue,
journal: entry.journal,
}));
// 更新引用列表状态
setReferences((prevReferences) => [
...prevReferences,
...newReferences,
]);
dataString = rawData
.map((entry) => {
return `Time: ${entry.year}\nTitle: ${entry.title}\nSummary: ${entry.abstract}\n\n`;
})
.join("");
}
// 确保搜索到的论文不超过 3000 个字符
const trimmedMessage =
dataString.length > 3000 ? dataString.slice(0, 3000) : dataString;
//slate的方法
// const content = `需要完成的论文主题:${topic}, 搜索到的论文内容:${trimmedMessage},之前已经完成的内容上下文:${extractText(
// editorValue
// )}`;
const content = `之前用户已经完成的内容上下文:${getTextBeforeCursor(
quill,
500
)},搜索到的论文内容:${trimmedMessage},需要完成的论文主题:${topic},请根据搜索到的论文内容完成用户的论文`;
sendMessageToOpenAI(content, quill, selectedModel, apiKey);
} catch (error) {
console.error("Error fetching data:", error);
// 错误处理
}
}
// 插入论文信息
// const insertPapers = async (topic: string) => {
// const rawData = await getArxivPapers(topic);
// const dataString = rawData
// .map((entry) => {
// return `ID: ${entry.id}\nPublished: ${entry.published}\nTitle: ${entry.title}\nSummary: ${entry.summary}\n\n`;
// })
// .join("");
// quill.insertText(quill.getLength(), dataString);
// };
return (
<div>
<div id="Qtoolbar" className="space-y-2 flex justify-between">
<input
type="text"
value={userInput}
onChange={handleInputChange}
className="flex-grow shadow appearance-none border rounded py-2 px-3 mr-2 text-grey-darker"
placeholder="点击AI Write就是正常的对话交流点击Paper2AI会根据输入的主题词去寻找对应论文" // 这是你的提示
/>
<button
onClick={handleAIWrite}
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 mr-2 rounded"
>
AI Write
</button>
{/* <button
onClick={() => insertPapers(userInput)}
className="bg-indigo-500 hover:bg-indigo-700 text-black font-bold py-2 px-4 rounded"
>
Insert Papers
</button> */}
<button
onClick={() => paper2AI(userInput)}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 mr-2 rounded"
>
Paper2AI
</button>
<select
value={selectedSource}
onChange={(e) => setSelectedSource(e.target.value)}
className=" border border-gray-300 bg-white py-2 px-3 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
>
<option value="arxiv">arxiv</option>
<option value="semanticScholar">semantic scholar</option>
{/* 其他来源网站 */}
</select>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className=" border border-gray-300 bg-white py-2 px-3 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
>
<option value="gpt3.5">gpt3.5</option>
<option value="gpt4">gpt4</option>
{/* 其他来源网站 */}
</select>
{/* 用户输入自己的API key */}
<button
onClick={() => formatTextInEditor(quill)} // 假设 updateIndex 是处理更新操作的函数
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 rounded"
>
</button>
</div>
<div>
<div
id="editor"
style={{
width: "calc(100vw - 100px)", // 屏幕宽度减去 100px
minHeight: "250px", // 注意驼峰命名法
maxHeight: "500px",
overflowY: "auto", // overflow-y -> overflowY
border: "1px solid #ccc",
padding: "10px",
}}
></div>
<ReferenceList
references={references}
addReference={addReference}
removeReference={removeReference}
setReferences={setReferences}
editor={quill}
/>
</div>
</div>
);
};
export default QEditor;