no message

This commit is contained in:
KuroSago 2025-07-10 15:41:31 +08:00
parent 00d512860d
commit 5c042b5d53
5 changed files with 402 additions and 355 deletions

View File

@ -0,0 +1,70 @@
<template>
<header
class="border-grid w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
>
<div class="container sticky flex items-center h-14">
<div class="hidden mr-4 md:mr-1 md:flex">
<a
href="/"
class="flex items-center mr-4 md:mr-2 lg:mr-6 lg:space-x1 xl:space-x-2"
>
<svg
viewBox="0 0 24 24"
width="24"
height="24"
class="mr-2"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 7v10" />
<path d="M8 9v6" opacity="0.7" />
<path d="M4 11v2" opacity="0.4" />
<path d="M16 9v6" opacity="0.7" />
<path d="M20 11v2" opacity="0.4" />
<rect
x="10"
y="5"
width="4"
height="14"
fill="currentColor"
opacity="0.1"
/>
</svg>
<span class="font-bold"> Echo Editor </span>
</a>
<nav class="flex items-center gap-4 text-sm xl:gap-6"></nav>
</div>
<div
class="flex items-center justify-between flex-1 space-x-2 md:justify-end"
>
<div class="flex-1 w-full md:w-auto md:flex-none"></div>
<nav class="flex items-center gap-0.5">
<a
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground w-8 h-8"
href="https://github.com/Seedsa/echo-editor"
target="_blank"
><svg
viewBox="0 0 15 15"
width="1.2em"
height="1.2em"
class="w-4 h-4"
>
<path
fill="currentColor"
fill-rule="evenodd"
d="M7.5.25a7.25 7.25 0 0 0-2.292 14.13c.363.066.495-.158.495-.35c0-.172-.006-.628-.01-1.233c-2.016.438-2.442-.972-2.442-.972c-.33-.838-.805-1.06-.805-1.06c-.658-.45.05-.441.05-.441c.728.051 1.11.747 1.11.747c.647 1.108 1.697.788 2.11.602c.066-.468.254-.788.46-.969c-1.61-.183-3.302-.805-3.302-3.583a2.8 2.8 0 0 1 .747-1.945c-.075-.184-.324-.92.07-1.92c0 0 .61-.194 1.994.744A7 7 0 0 1 7.5 3.756A7 7 0 0 1 9.315 4c1.384-.938 1.992-.743 1.992-.743c.396.998.147 1.735.072 1.919c.465.507.745 1.153.745 1.945c0 2.785-1.695 3.398-3.31 3.577c.26.224.492.667.492 1.343c0 .97-.009 1.751-.009 1.989c0 .194.131.42.499.349A7.25 7.25 0 0 0 7.499.25"
clip-rule="evenodd"
></path></svg></a
><ThemeToggle />
</nav>
</div>
</div>
</header>
</template>
<script setup lang="ts">
import { ThemeToggle } from "echo-editor";
</script>

View File

@ -0,0 +1,58 @@
<template>
<div class="mb-2">
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="locale.setLang('zhHans')"
>
中文
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="locale.setLang('en')"
>
English
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="emit('toggle-minimal')"
>
{{ minimal ? "Full" : "Minimal" }}
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="emit('toggle-toolbar')"
>
{{ !hideToolbar ? "Hide Toolbar" : "Show Toolbar" }}
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="emit('toggle-menubar')"
>
{{ !hideMenubar ? "Hide Menubar" : "Show Menubar" }}
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="emit('toggle-editable')"
>
{{ disabled ? "Editable" : "Readonly" }}
</button>
</div>
</template>
<script setup lang="ts">
import { locale } from "echo-editor";
defineProps<{
minimal: boolean;
hideToolbar: boolean;
hideMenubar: boolean;
disabled: boolean;
}>();
const emit = defineEmits([
"toggle-minimal",
"toggle-toolbar",
"toggle-menubar",
"toggle-editable",
]);
</script>

View File

@ -0,0 +1,14 @@
<template>
<div class="p-4 mt-6 border rounded-lg bg-muted">
<h3 class="mb-2 text-sm font-medium">HTML Output</h3>
<div class="rounded bg-muted-foreground/5 max-h-[500px] overflow-auto">
<span>{{ content }}</span>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
content: string;
}>();
</script>

View File

@ -0,0 +1,241 @@
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,
};
}

View File

@ -1,109 +1,18 @@
<template>
<div class="min-h-screen bg-background">
<header
class="border-grid w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
>
<div class="container sticky flex items-center h-14">
<div class="hidden mr-4 md:mr-1 md:flex">
<a
href="/"
class="flex items-center mr-4 md:mr-2 lg:mr-6 lg:space-x1 xl:space-x-2"
>
<svg
viewBox="0 0 24 24"
width="24"
height="24"
class="mr-2"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 7v10" />
<path d="M8 9v6" opacity="0.7" />
<path d="M4 11v2" opacity="0.4" />
<path d="M16 9v6" opacity="0.7" />
<path d="M20 11v2" opacity="0.4" />
<rect
x="10"
y="5"
width="4"
height="14"
fill="currentColor"
opacity="0.1"
/>
</svg>
<span class="font-bold"> Echo Editor </span>
</a>
<nav class="flex items-center gap-4 text-sm xl:gap-6"></nav>
</div>
<div
class="flex items-center justify-between flex-1 space-x-2 md:justify-end"
>
<div class="flex-1 w-full md:w-auto md:flex-none"></div>
<nav class="flex items-center gap-0.5">
<a
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground w-8 h-8"
href="https://github.com/Seedsa/echo-editor"
target="_blank"
><svg
viewBox="0 0 15 15"
width="1.2em"
height="1.2em"
class="w-4 h-4"
>
<path
fill="currentColor"
fill-rule="evenodd"
d="M7.5.25a7.25 7.25 0 0 0-2.292 14.13c.363.066.495-.158.495-.35c0-.172-.006-.628-.01-1.233c-2.016.438-2.442-.972-2.442-.972c-.33-.838-.805-1.06-.805-1.06c-.658-.45.05-.441.05-.441c.728.051 1.11.747 1.11.747c.647 1.108 1.697.788 2.11.602c.066-.468.254-.788.46-.969c-1.61-.183-3.302-.805-3.302-3.583a2.8 2.8 0 0 1 .747-1.945c-.075-.184-.324-.92.07-1.92c0 0 .61-.194 1.994.744A7 7 0 0 1 7.5 3.756A7 7 0 0 1 9.315 4c1.384-.938 1.992-.743 1.992-.743c.396.998.147 1.735.072 1.919c.465.507.745 1.153.745 1.945c0 2.785-1.695 3.398-3.31 3.577c.26.224.492.667.492 1.343c0 .97-.009 1.751-.009 1.989c0 .194.131.42.499.349A7.25 7.25 0 0 0 7.499.25"
clip-rule="evenodd"
></path></svg></a
><ThemeToggle />
</nav>
</div>
</div>
</header>
<AppHeader />
<div class="my-0 mx-auto max-w-[1024px] p-6">
<div class="mb-2">
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="locale.setLang('zhHans')"
>
中文
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="locale.setLang('en')"
>
English
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="toggleMinimal"
>
{{ minimal ? "Full" : "Minimal" }}
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="hideToolbar = !hideToolbar"
>
{{ !hideToolbar ? "Hide Toolbar" : "Show Toolbar" }}
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="hideMenubar = !hideMenubar"
>
{{ !hideMenubar ? "Hide Menubar" : "Show Menubar" }}
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="disabled = !disabled"
>
{{ disabled ? "Editable" : "Readonly" }}
</button>
</div>
<EditorActions
:minimal="minimal"
:hide-toolbar="hideToolbar"
:hide-menubar="hideMenubar"
:disabled="disabled"
@toggle-minimal="toggleMinimal"
@toggle-toolbar="hideToolbar = !hideToolbar"
@toggle-menubar="hideMenubar = !hideMenubar"
@toggle-editable="disabled = !disabled"
/>
<div class="border rounded-lg shadow-sm bg-card text-card-foreground">
<echo-editor
v-model="content"
@ -118,66 +27,18 @@
>
</echo-editor>
</div>
<div class="p-4 mt-6 border rounded-lg bg-muted">
<h3 class="mb-2 text-sm font-medium">HTML Output</h3>
<div class="rounded bg-muted-foreground/5 max-h-[500px] overflow-auto">
<span>{{ content }}</span>
</div>
</div>
<HtmlOutput :content="content" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, 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,
locale,
ImportWord,
Columns,
TextAlign,
ImageUpload,
VideoUpload,
FontFamily,
FindAndReplace,
Code,
AI,
Preview,
Printer,
Iframe,
EchoEditor,
ThemeToggle,
SpecialCharacter,
SourceCode,
} from "echo-editor";
import { ExportWord } from "../components/ExportWord";
import OpenAI from "openai";
import { ref } from "vue";
import { EchoEditor } from "echo-editor";
import { DEMO_CONTENT } from "./initContent";
import AppHeader from "../components/AppHeader.vue";
import EditorActions from "../components/EditorActions.vue";
import HtmlOutput from "../components/HtmlOutput.vue";
import { useEditorConfig } from "../components/useEditorConfig";
import "./style.css";
import "echo-editor/style.css";
@ -189,206 +50,9 @@ const hideMenubar = ref<boolean>(false);
const disabled = ref<boolean>(false);
const minimal = ref(false);
const extensions = computed(() =>
minimal.value ? minimalExtensions : fullExtensions
);
const { extensions } = useEditorConfig(minimal.value);
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);
}
function toggleMinimal() {
minimal.value = !minimal.value;
}
/**
* AI Completions handler function
* WARNING: This is just a demo implementation. In production:
* - DO NOT expose API keys in the frontend
* - DO implement this through your backend API
* - DO add proper error handling and rate limiting
*
* @param history - Chat history array containing messages with role and content
* @param signal - AbortSignal for cancelling requests
* @returns OpenAI chat completion stream
*/
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;
}
}
</script>