diff --git a/web3/apps/web/src/views/tiptap/index.vue b/web3/apps/web/src/views/tiptap/index.vue index 1ece3f4f..36e88aca 100644 --- a/web3/apps/web/src/views/tiptap/index.vue +++ b/web3/apps/web/src/views/tiptap/index.vue @@ -1,15 +1,23 @@ diff --git a/web3/packages/tiptap/src/components/useEditorConfig.ts b/web3/packages/tiptap/src/components/useEditorConfig.ts deleted file mode 100644 index 452806d9..00000000 --- a/web3/packages/tiptap/src/components/useEditorConfig.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { computed } from "vue"; -import { - Bold, - BulletList, - Italic, - BaseKit, - Underline, - Strike, - LineHeight, - Image, - History, - Heading, - CodeBlock, - FontSize, - Highlight, - Table, - Clear, - Blockquote, - Link, - Color, - Video, - OrderedList, - HorizontalRule, - Fullscreen, - TaskList, - MoreMark, - FormatPainter, - SlashCommand, - Indent, - ImportWord, - Columns, - TextAlign, - ImageUpload, - VideoUpload, - FontFamily, - FindAndReplace, - Code, - AI, - Preview, - Printer, - Iframe, - SpecialCharacter, - SourceCode, -} from "echo-editor"; -import { ExportWord } from "./ExportWord"; -import OpenAI from "openai"; - -export function useEditorConfig(minimal: boolean) { - const extensions = computed(() => - minimal ? minimalExtensions : fullExtensions - ); - - const minimalExtensions = [ - BaseKit.configure({ - characterCount: { - limit: 50000, - }, - }), - Heading, - Bold.configure({ spacer: true }), - Italic, - Underline, - HorizontalRule, - TextAlign.configure({ - types: ["heading", "paragraph", "image"], - spacer: true, - }), - Image, - Blockquote.configure({ spacer: true }), - Code, - Link, - Color, - TaskList.configure({ spacer: true }), - OrderedList, - BulletList, - ]; - const fullExtensions = [ - BaseKit.configure({ - placeholder: { - showOnlyCurrent: true, - }, - characterCount: { - limit: 50000, - }, - }), - History, - Columns, - FormatPainter, - Clear, - Heading.configure({ spacer: true }), - FontSize, - FontFamily, - Bold, - Italic, - Underline, - Strike, - MoreMark, - Color.configure({ spacer: true }), - Highlight, - BulletList, - OrderedList, - TextAlign.configure({ - types: ["heading", "paragraph", "image"], - spacer: true, - }), - Indent, - LineHeight, - TaskList.configure({ - spacer: true, - taskItem: { - nested: true, - }, - }), - Link, - Image, - ImageUpload.configure({ - upload: (files: File) => { - return new Promise((resolve) => { - setTimeout(() => { - resolve(URL.createObjectURL(files)); - }, 3000); - }); - }, - }), - Video, - VideoUpload.configure({ - upload: handleFileUpload, - }), - Blockquote, - SlashCommand, - HorizontalRule, - CodeBlock, - Table.configure({ spacer: true }), - Code, - ExportWord, - AI.configure({ - completions: AICompletions, - shortcuts: [ - // 这里可以传入额外的自定义shortcuts - { - label: "Custom Actions", - children: [ - { - label: "This is Custom Action", - prompt: - "Rewrite this content with no spelling mistakes, proper grammar, and with more descriptive language, using best writing practices without losing the original meaning.", - }, - ], - }, - ], - }), - ImportWord.configure({ - upload: handleFileUpload, - }), - SpecialCharacter, - Fullscreen.configure({ spacer: true }), - SourceCode, - Preview, - FindAndReplace.configure({ spacer: true }), - Printer, - Iframe, - ]; - async function handleFileUpload(files: File[]) { - const f = files.map((file) => ({ - src: URL.createObjectURL(file), - alt: file.name, - })); - return Promise.resolve(f); - } - - async function AICompletions( - history: Array<{ role: string; content: string }> = [], - signal?: AbortSignal - ) { - // groq.com For free llm api recommend deepseek r1 70b - // SECURITY WARNING: API keys should never be exposed in the frontend - // This is just for demo purposes - // In production, implement this through your backend API - const apiKey = "import.meta.env.VITE_OPENAI_API_KEY"; - const baseURL = "import.meta.env.VITE_OPENAI_BASE_URL"; - const model = "import.meta.env.VITE_OPENAI_MODEL"; - - if (!apiKey || !baseURL || !model) { - throw new Error( - "OpenAI configuration is missing. Please check your environment variables." - ); - } - - const openai = new OpenAI({ - apiKey: apiKey, - dangerouslyAllowBrowser: true, - baseURL: baseURL, - }); - - const systemMsg = `You are a professional writing assistant. Please respond based on the user's context: - -1. Maintain a professional, accurate, and objective tone -2. Ensure responses are clear, coherent, and well-structured -3. Responses must be in HTML format, preserving all HTML tags, links, and styles -4. Support the following writing enhancements: - - Grammar and spelling corrections - - Improved sentence structure and expression - - Optimized article formatting and layout - - Maintain the core meaning of the original text -5. If context includes code, maintain code formatting and provide optimization suggestions -6. Add appropriate HTML elements like headings, lists, quotes etc. to enhance readability as needed - -Please respond only based on the provided context, do not add irrelevant information.`; - - const systemPrompt = [{ role: "system", content: systemMsg }]; - const finalMessages = [...systemPrompt]; - - if (history.length > 0) { - finalMessages.push(...history); - } - - try { - const stream = await openai.chat.completions.create( - { - model, - messages: finalMessages, - temperature: 0.7, - stream: true, - reasoning_format: "parsed", // groq deepseek r1 need this - } as any, - { signal } - ); - - return stream; - } catch (error) { - if (error instanceof Error) { - console.error("Error in AI Completions:", error.message); - } - throw error; - } - } - - return { - extensions, - }; -} diff --git a/web3/packages/tiptap/src/context/consts.ts b/web3/packages/tiptap/src/context/consts.ts new file mode 100644 index 00000000..0c717a28 --- /dev/null +++ b/web3/packages/tiptap/src/context/consts.ts @@ -0,0 +1,232 @@ +import { ExportWord } from "../components/ExportWord"; +import OpenAI from "openai"; + +import { + Bold, + BulletList, + Italic, + BaseKit, + Underline, + Strike, + LineHeight, + Image, + History, + Heading, + CodeBlock, + FontSize, + Highlight, + Table, + Clear, + Blockquote, + Link, + Color, + Video, + OrderedList, + HorizontalRule, + Fullscreen, + TaskList, + MoreMark, + FormatPainter, + SlashCommand, + Indent, + ImportWord, + Columns, + TextAlign, + ImageUpload, + VideoUpload, + FontFamily, + FindAndReplace, + Code, + AI, + Preview, + Printer, + Iframe, + SpecialCharacter, + SourceCode, +} from "echo-editor"; + +async function handleFileUpload(files: File[]) { + const f = files.map((file) => ({ + src: URL.createObjectURL(file), + alt: file.name, + })); + return Promise.resolve(f); +} + +async function AICompletions( + history: Array<{ role: string; content: string }> = [], + signal?: AbortSignal +) { + // groq.com For free llm api recommend deepseek r1 70b + // SECURITY WARNING: API keys should never be exposed in the frontend + // This is just for demo purposes + // In production, implement this through your backend API + const apiKey = "import.meta.env.VITE_OPENAI_API_KEY"; + const baseURL = "import.meta.env.VITE_OPENAI_BASE_URL"; + const model = "import.meta.env.VITE_OPENAI_MODEL"; + + if (!apiKey || !baseURL || !model) { + throw new Error( + "OpenAI configuration is missing. Please check your environment variables." + ); + } + + const openai = new OpenAI({ + apiKey: apiKey, + dangerouslyAllowBrowser: true, + baseURL: baseURL, + }); + + const systemMsg = `You are a professional writing assistant. Please respond based on the user's context: + +1. Maintain a professional, accurate, and objective tone +2. Ensure responses are clear, coherent, and well-structured +3. Responses must be in HTML format, preserving all HTML tags, links, and styles +4. Support the following writing enhancements: + - Grammar and spelling corrections + - Improved sentence structure and expression + - Optimized article formatting and layout + - Maintain the core meaning of the original text +5. If context includes code, maintain code formatting and provide optimization suggestions +6. Add appropriate HTML elements like headings, lists, quotes etc. to enhance readability as needed + +Please respond only based on the provided context, do not add irrelevant information.`; + + const systemPrompt = [{ role: "system", content: systemMsg }]; + const finalMessages = [...systemPrompt]; + + if (history.length > 0) { + finalMessages.push(...history); + } + + try { + const stream = await openai.chat.completions.create( + { + model, + messages: finalMessages, + temperature: 0.7, + stream: true, + reasoning_format: "parsed", // groq deepseek r1 need this + } as any, + { signal } + ); + + return stream; + } catch (error) { + if (error instanceof Error) { + console.error("Error in AI Completions:", error.message); + } + throw error; + } +} + +export const minimalExtensions = [ + BaseKit.configure({ + characterCount: { + limit: 50000, + }, + }), + Heading, + Bold.configure({ spacer: true }), + Italic, + Underline, + HorizontalRule, + TextAlign.configure({ + types: ["heading", "paragraph", "image"], + spacer: true, + }), + Image, + Blockquote.configure({ spacer: true }), + Code, + Link, + Color, + TaskList.configure({ spacer: true }), + OrderedList, + BulletList, +]; +export const fullExtensions = [ + BaseKit.configure({ + placeholder: { + showOnlyCurrent: true, + }, + characterCount: { + limit: 50000, + }, + }), + History, + Columns, + FormatPainter, + Clear, + Heading.configure({ spacer: true }), + FontSize, + FontFamily, + Bold, + Italic, + Underline, + Strike, + MoreMark, + Color.configure({ spacer: true }), + Highlight, + BulletList, + OrderedList, + TextAlign.configure({ + types: ["heading", "paragraph", "image"], + spacer: true, + }), + Indent, + LineHeight, + TaskList.configure({ + spacer: true, + taskItem: { + nested: true, + }, + }), + Link, + Image, + ImageUpload.configure({ + upload: (files: File) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(URL.createObjectURL(files)); + }, 3000); + }); + }, + }), + Video, + VideoUpload.configure({ + upload: handleFileUpload, + }), + Blockquote, + SlashCommand, + HorizontalRule, + CodeBlock, + Table.configure({ spacer: true }), + Code, + ExportWord, + AI.configure({ + completions: AICompletions, + shortcuts: [ + // 这里可以传入额外的自定义shortcuts + { + label: "Custom Actions", + children: [ + { + label: "This is Custom Action", + prompt: + "Rewrite this content with no spelling mistakes, proper grammar, and with more descriptive language, using best writing practices without losing the original meaning.", + }, + ], + }, + ], + }), + // ImportWord.configure({ + // upload: handleFileUpload, + // }), + SpecialCharacter, + Fullscreen.configure({ spacer: true }), + SourceCode, + Preview, + FindAndReplace.configure({ spacer: true }), + Printer, + Iframe, +]; diff --git a/web3/packages/tiptap/src/context/useTiptapStore.ts b/web3/packages/tiptap/src/context/useTiptapStore.ts index aa1ab113..72652520 100644 --- a/web3/packages/tiptap/src/context/useTiptapStore.ts +++ b/web3/packages/tiptap/src/context/useTiptapStore.ts @@ -1,4 +1,13 @@ -import { provide, inject, reactive, type InjectionKey } from "vue"; +import { + provide, + inject, + reactive, + type InjectionKey, + computed, + type ComputedRef, +} from "vue"; + +import { minimalExtensions, fullExtensions } from "./consts"; const TiptapStoreKey: InjectionKey = Symbol("TiptapStore"); @@ -13,6 +22,7 @@ export interface TiptapState { export type TiptapStore = { state: TiptapState; + extensions: ComputedRef; }; export function useTiptapStore(initialState?: Partial) { @@ -26,8 +36,13 @@ export function useTiptapStore(initialState?: Partial) { ...initialState, }); + const extensions = computed(() => { + return state.minimal ? minimalExtensions : fullExtensions; + }); + const store: TiptapStore = { state: state, + extensions, }; provide(TiptapStoreKey, store); diff --git a/web3/packages/tiptap/src/index.ts b/web3/packages/tiptap/src/index.ts index 7ffd635d..a7f1d1fc 100644 --- a/web3/packages/tiptap/src/index.ts +++ b/web3/packages/tiptap/src/index.ts @@ -2,6 +2,7 @@ import TiptapEditor from "./views/index.vue"; import { useTiptapStore, useTiptapContext } from "./context/useTiptapStore"; import { ThemeToggle } from "echo-editor"; import EditorActions from "./components/EditorActions.vue"; +import { DEMO_CONTENT } from "./views/initContent"; export { TiptapEditor, @@ -9,4 +10,5 @@ export { useTiptapStore, useTiptapContext, EditorActions, + DEMO_CONTENT, }; diff --git a/web3/packages/tiptap/src/views/index.vue b/web3/packages/tiptap/src/views/index.vue index f9cb86e7..277d7da4 100644 --- a/web3/packages/tiptap/src/views/index.vue +++ b/web3/packages/tiptap/src/views/index.vue @@ -1,9 +1,8 @@