edit

68 阅读18分钟

edit

"use client";
import "@copilotkit/react-ui/styles.css";
import "./style.css";

import MarkdownIt from "markdown-it";
import { diffWords } from "diff";
import { useEffect, useState } from "react";
import {
  CopilotKit,
  useCoAgent,
  useCopilotAction,
  useCopilotChat,
} from "@copilotkit/react-core";
import { CopilotSidebar, useCopilotChatSuggestions } from "@copilotkit/react-ui";
import { initialPrompt, chatSuggestions } from "@/lib/prompts";
import {
  EditorCommand,
  EditorCommandEmpty,
  EditorCommandItem,
  EditorCommandList,
  EditorContent as NovelEditorContent,
  type EditorInstance,
  EditorRoot,
  ImageResizer,
  type JSONContent,
  handleCommandNavigation,
  handleImageDrop,
  handleImagePaste,
} from "novel";
import { useDebouncedCallback } from "use-debounce";
import { defaultExtensions } from "@/components/editor/extensions";
import { ColorSelector } from "@/components/editor/selectors/color-selector";
import { LinkSelector } from "@/components/editor/selectors/link-selector";
import { MathSelector } from "@/components/editor/selectors/math-selector";
import { NodeSelector } from "@/components/editor/selectors/node-selector";
import { Separator } from "@/components/ui/separator";
import GenerativeMenuSwitch from "@/components/editor/generative/generative-menu-switch";
import { uploadFn } from "@/components/editor/image-upload";
import { TextButtons } from "@/components/editor/selectors/text-buttons";
import { slashCommand, suggestionItems } from "@/components/editor/slash-command";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { 
  FileText, 
  Save, 
  Share2, 
  Download, 
  Settings, 
  Maximize2, 
  Minimize2, 
  Moon, 
  Sun, 
  Zap,
  Clock,
  Users,
  Eye,
  MoreHorizontal,
  Sparkles,
  Brain,
  Wand2,
  Timer,
  CheckCircle2,
  AlertCircle,
  Info
} from "lucide-react";
import { useTheme } from "next-themes";
import "@/styles/prosemirror.css";

const hljs = require("highlight.js");
const extensions = [...defaultExtensions, slashCommand];

export default function PredictiveStateUpdates() {
  return (
    <CopilotKit
      runtimeUrl={"/api/copilotkit"}
      showDevConsole={false}
      agent="sample_agent"
    >
      <DocumentEditorApp />
    </CopilotKit>
  );
}

const DocumentEditorApp = () => {
  const [isFullscreen, setIsFullscreen] = useState(false);
  const [sidebarOpen, setSidebarOpen] = useState(true);
  const { theme, setTheme } = useTheme();

  return (
    <div className="h-screen w-full flex flex-col relative overflow-hidden"
         style={{
           "--copilot-kit-primary-color": "#222",
           "--copilot-kit-separator-color": "#CCC",
         } as React.CSSProperties}>
      
      {/* 动态背景效果 */}
      <div className="absolute inset-0 bg-gradient-to-br from-purple-50 via-blue-50 to-cyan-50 dark:from-purple-950/20 dark:via-blue-950/20 dark:to-cyan-950/20" />
      <div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,_rgba(255,255,255,0.3)_1px,_transparent_0)] dark:bg-[radial-gradient(circle_at_1px_1px,_rgba(255,255,255,0.1)_1px,_transparent_0)] bg-[size:20px_20px] opacity-60" />
      
      {/* 浮动装饰元素 */}
      <div className="absolute top-20 left-20 w-64 h-64 bg-gradient-to-r from-purple-400/20 to-blue-400/20 rounded-full blur-3xl animate-pulse" />
      <div className="absolute bottom-20 right-20 w-96 h-96 bg-gradient-to-r from-cyan-400/20 to-blue-400/20 rounded-full blur-3xl animate-pulse animation-delay-2000" />
      <div className="absolute top-1/2 left-1/3 w-32 h-32 bg-gradient-to-r from-pink-400/20 to-purple-400/20 rounded-full blur-2xl animate-bounce animation-delay-1000" />
      
      {/* 主要内容 */}
      <div className="relative z-10 h-screen flex flex-col">
        {/* 顶部导航栏 */}
        <TopNavigation 
          isFullscreen={isFullscreen}
          onToggleFullscreen={() => setIsFullscreen(!isFullscreen)}
          theme={theme}
          onThemeChange={setTheme}
        />
        
        {/* 主要内容区域 */}
        <div className="flex-1 flex overflow-hidden">
          {/* 文档编辑区域 */}
          <div className="flex-1 flex flex-col">
            {/* 工具栏 */}
            <EditorToolbar />
            
            {/* 编辑器区域 */}
            <div className="flex-1 flex">
              <div className="flex-1 p-6">
                <DocumentEditor />
              </div>
              
              {/* AI助手侧边栏 */}
              <CopilotSidebar
                defaultOpen={sidebarOpen}
                labels={{
                  title: "AI写作助手",
                  initial: initialPrompt.sample_agent,
                }}
                clickOutsideToClose={false}
                className="w-96 border-l border-gray-200/50 dark:border-gray-700/50 bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl"
              />
            </div>
          </div>
        </div>
        
        {/* 底部状态栏 */}
        <StatusBar />
      </div>
    </div>
  );
};

const TopNavigation = ({ 
  isFullscreen, 
  onToggleFullscreen, 
  theme, 
  onThemeChange 
}: {
  isFullscreen: boolean;
  onToggleFullscreen: () => void;
  theme: string | undefined;
  onThemeChange: (theme: string) => void;
}) => {
  return (
    <header className="h-16 bg-white/80 dark:bg-gray-900/80 backdrop-blur-xl border-b border-gray-200/30 dark:border-gray-700/30 flex items-center justify-between px-6 relative">
      {/* 顶部渐变装饰 */}
      <div className="absolute top-0 left-0 right-0 h-[1px] bg-gradient-to-r from-purple-500 via-blue-500 to-cyan-500" />
      
      <div className="flex items-center space-x-6">
        <div className="flex items-center space-x-3">
          {/* Logo */}
          <div className="relative group">
            <div className="w-10 h-10 bg-gradient-to-r from-purple-500 via-blue-500 to-cyan-500 rounded-xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-all duration-300 group-hover:scale-110">
              <Sparkles className="w-5 h-5 text-white group-hover:rotate-12 transition-transform duration-300" />
            </div>
            <div className="absolute inset-0 w-10 h-10 bg-gradient-to-r from-purple-500 via-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-30 blur-lg transition-all duration-300" />
          </div>
          
          {/* 标题 */}
          <div>
            <h1 className="text-xl font-bold bg-gradient-to-r from-gray-900 via-blue-800 to-purple-800 dark:from-white dark:via-blue-200 dark:to-purple-200 bg-clip-text text-transparent">
              AI文档编辑器
            </h1>
            <p className="text-xs text-gray-500 dark:text-gray-400">智能写作,创意无限</p>
          </div>
        </div>
        
        {/* 状态指示器 */}
        <div className="flex items-center space-x-3">
          <Badge variant="secondary" className="bg-gradient-to-r from-emerald-50 to-green-50 dark:from-emerald-900/20 dark:to-green-900/20 border-emerald-200 dark:border-emerald-700 text-emerald-700 dark:text-emerald-300">
            <div className="w-2 h-2 bg-emerald-500 rounded-full mr-2 animate-pulse" />
            AI在线
          </Badge>
          <Badge variant="outline" className="bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 border-blue-200 dark:border-blue-700 text-blue-700 dark:text-blue-300">
            <Zap className="w-3 h-3 mr-1" />
            AI增强
          </Badge>
        </div>
      </div>
      
      <div className="flex items-center space-x-2">
        <TooltipProvider>
          {/* 保存按钮 */}
          <Tooltip>
            <TooltipTrigger asChild>
              <Button variant="ghost" size="sm" className="relative group overflow-hidden hover:bg-gradient-to-r hover:from-emerald-50 hover:to-green-50 dark:hover:from-emerald-900/20 dark:hover:to-green-900/20 border border-transparent hover:border-emerald-200 dark:hover:border-emerald-700 transition-all duration-300">
                <Save className="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover:text-emerald-600 dark:group-hover:text-emerald-400 transition-colors duration-300" />
              </Button>
            </TooltipTrigger>
            <TooltipContent>保存文档</TooltipContent>
          </Tooltip>
          
          {/* 分享按钮 */}
          <Tooltip>
            <TooltipTrigger asChild>
              <Button variant="ghost" size="sm" className="relative group overflow-hidden hover:bg-gradient-to-r hover:from-blue-50 hover:to-purple-50 dark:hover:from-blue-900/20 dark:hover:to-purple-900/20 border border-transparent hover:border-blue-200 dark:hover:border-blue-700 transition-all duration-300">
                <Share2 className="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-300" />
              </Button>
            </TooltipTrigger>
            <TooltipContent>分享文档</TooltipContent>
          </Tooltip>
          
          {/* 导出按钮 */}
          <Tooltip>
            <TooltipTrigger asChild>
              <Button variant="ghost" size="sm" className="relative group overflow-hidden hover:bg-gradient-to-r hover:from-orange-50 hover:to-red-50 dark:hover:from-orange-900/20 dark:hover:to-red-900/20 border border-transparent hover:border-orange-200 dark:hover:border-orange-700 transition-all duration-300">
                <Download className="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover:text-orange-600 dark:group-hover:text-orange-400 transition-colors duration-300" />
              </Button>
            </TooltipTrigger>
            <TooltipContent>导出文档</TooltipContent>
          </Tooltip>
          
          <Separator orientation="vertical" className="h-4" />
          
          {/* 主题切换 */}
          <Tooltip>
            <TooltipTrigger asChild>
              <Button 
                variant="ghost" 
                size="sm"
                onClick={() => onThemeChange(theme === 'dark' ? 'light' : 'dark')}
                className="relative group overflow-hidden hover:bg-gradient-to-r hover:from-yellow-50 hover:to-orange-50 dark:hover:from-yellow-900/20 dark:hover:to-orange-900/20 border border-transparent hover:border-yellow-200 dark:hover:border-yellow-700 transition-all duration-300"
              >
                {theme === 'dark' ? (
                  <Sun className="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover:text-yellow-600 dark:group-hover:text-yellow-400 transition-colors duration-300" />
                ) : (
                  <Moon className="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-300" />
                )}
              </Button>
            </TooltipTrigger>
            <TooltipContent>切换主题</TooltipContent>
          </Tooltip>
          
          {/* 全屏按钮 */}
          <Tooltip>
            <TooltipTrigger asChild>
              <Button variant="ghost" size="sm" onClick={onToggleFullscreen} className="relative group overflow-hidden hover:bg-gradient-to-r hover:from-purple-50 hover:to-pink-50 dark:hover:from-purple-900/20 dark:hover:to-pink-900/20 border border-transparent hover:border-purple-200 dark:hover:border-purple-700 transition-all duration-300">
                {isFullscreen ? (
                  <Minimize2 className="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors duration-300" />
                ) : (
                  <Maximize2 className="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors duration-300" />
                )}
              </Button>
            </TooltipTrigger>
            <TooltipContent>全屏模式</TooltipContent>
          </Tooltip>
          
          {/* 设置按钮 */}
          <Button variant="ghost" size="sm" className="relative group overflow-hidden hover:bg-gradient-to-r hover:from-gray-50 hover:to-slate-50 dark:hover:from-gray-800/50 dark:hover:to-slate-800/50 border border-transparent hover:border-gray-200 dark:hover:border-gray-700 transition-all duration-300">
            <Settings className="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover:text-gray-800 dark:group-hover:text-gray-200 transition-colors duration-300" />
          </Button>
        </TooltipProvider>
      </div>
    </header>
  );
};

const EditorToolbar = () => {
  return (
    <div className="h-14 bg-white/90 dark:bg-gray-900/90 backdrop-blur-xl border-b border-gray-200/30 dark:border-gray-700/30 flex items-center px-6 relative">
      {/* 装饰性渐变线 */}
      <div className="absolute bottom-0 left-0 right-0 h-[1px] bg-gradient-to-r from-transparent via-purple-500/50 to-transparent" />
      
      <div className="flex items-center justify-between w-full">
        <div className="flex items-center space-x-6">
          {/* AI状态卡片 */}
          <div className="flex items-center space-x-3">
            <div className="relative group">
              <Button variant="ghost" size="sm" className="bg-gradient-to-r from-purple-500/10 to-blue-500/10 hover:from-purple-500/20 hover:to-blue-500/20 border border-purple-200/50 dark:border-purple-700/50 text-purple-700 dark:text-purple-300 transition-all duration-300 hover:scale-105">
                <div className="flex items-center space-x-2">
                  <div className="w-5 h-5 bg-gradient-to-r from-purple-500 to-blue-500 rounded-lg flex items-center justify-center">
                    <Brain className="w-3 h-3 text-white" />
                  </div>
                  <span className="font-medium">AI助手已激活</span>
                </div>
              </Button>
              
              {/* 脉冲效果 */}
              <div className="absolute inset-0 bg-gradient-to-r from-purple-500/20 to-blue-500/20 rounded-lg animate-pulse opacity-50" />
            </div>
            
            <Badge variant="outline" className="bg-gradient-to-r from-emerald-50 to-green-50 dark:from-emerald-900/20 dark:to-green-900/20 border-emerald-200 dark:border-emerald-700 text-emerald-700 dark:text-emerald-300 animate-pulse">
              <Sparkles className="w-3 h-3 mr-1" />
              选中文字显示AI工具
            </Badge>
          </div>
        </div>
        
        {/* 使用提示 */}
        <div className="flex items-center space-x-4">
          <div className="flex items-center space-x-2 text-sm text-gray-600 dark:text-gray-400">
            <div className="w-6 h-6 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full flex items-center justify-center">
              <span className="text-white text-xs font-bold">💡</span>
            </div>
            <span className="hidden md:block">选中文字后点击 "Ask AI" 进行润色、扩写、缩短等操作</span>
            <span className="md:hidden">选中文字使用AI功能</span>
          </div>
        </div>
      </div>
    </div>
  );
};

interface AgentState {
  document: string;
}

const DocumentEditor = () => {
  const [placeholderVisible, setPlaceholderVisible] = useState(false);
  const [currentDocument, setCurrentDocument] = useState("");
  const [saveStatus, setSaveStatus] = useState("已保存");
  const [editorInstance, setEditorInstance] = useState<EditorInstance | null>(null);
  const [wordCount, setWordCount] = useState(0);
  const [readingTime, setReadingTime] = useState(0);
  const { isLoading } = useCopilotChat();

  // AI工具栏状态
  const [openNode, setOpenNode] = useState(false);
  const [openColor, setOpenColor] = useState(false);
  const [openLink, setOpenLink] = useState(false);
  const [openAI, setOpenAI] = useState(false);

  const {
    state: agentState,
    setState: setAgentState,
    nodeName,
  } = useCoAgent<AgentState>({
    name: "sample_agent",
    initialState: {
      document: "",
    },
  });

  const debouncedUpdates = useDebouncedCallback(
    async (editor: EditorInstance) => {
      if (!isLoading) {
        const text = editor.getText();
        const markdown = editor.storage.markdown.getMarkdown();
        
        setCurrentDocument(text);
        setWordCount(text.trim().split(/\s+/).filter(word => word.length > 0).length);
        setReadingTime(Math.ceil(text.split(' ').length / 200)); // 假设每分钟200字
        
        setAgentState({
          document: text,
        });
        setSaveStatus("已保存");
      }
    },
    500,
  );

  useEffect(() => {
    if (editorInstance) {
      editorInstance.setEditable(!isLoading);
    }
  }, [isLoading, editorInstance]);

  useEffect(() => {
    if (nodeName === "end" && editorInstance) {
      if (
        currentDocument.trim().length > 0 &&
        currentDocument !== agentState?.document
      ) {
        const newDocument = agentState?.document || "";
        const diff = diffPartialText(currentDocument, newDocument, true);
        const markdown = fromMarkdown(diff);
        editorInstance.commands.setContent(markdown);
      }
    }
  }, [nodeName, editorInstance, currentDocument, agentState?.document]);

  useEffect(() => {
    if (isLoading && editorInstance) {
      if (currentDocument.trim().length > 0) {
        const newDocument = agentState?.document || "";
        const diff = diffPartialText(currentDocument, newDocument);
        const markdown = fromMarkdown(diff);
        editorInstance.commands.setContent(markdown);
      } else {
        const markdown = fromMarkdown(agentState?.document || "");
        editorInstance.commands.setContent(markdown);
      }
    }
  }, [agentState?.document, isLoading, editorInstance, currentDocument]);

  const text = editorInstance?.getText() || "";

  useEffect(() => {
    setPlaceholderVisible(text.length === 0);
  }, [text]);

  useCopilotAction({
    name: "confirm_changes",
    renderAndWaitForResponse: ({ args, respond, status }) => {
      const [accepted, setAccepted] = useState<boolean | null>(null);
      return (
        <Card className="m-4 shadow-lg">
          <CardContent className="p-6">
            <div className="flex items-start space-x-3">
              <div className="flex-shrink-0">
                <AlertCircle className="w-6 h-6 text-amber-500" />
              </div>
              <div className="flex-1">
                <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
                  确认AI建议的更改
                </h3>
                <p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
                  AI助手建议对您的文档进行以下更改,请确认是否接受。
                </p>
              </div>
            </div>
            
            {accepted === null && (
              <div className="mt-6 flex justify-end space-x-3">
                <Button
                  variant="outline"
                  disabled={status !== "executing"}
                  onClick={() => {
                    if (respond && editorInstance) {
                      setAccepted(false);
                      editorInstance.commands.setContent(fromMarkdown(currentDocument));
                      setAgentState({ document: currentDocument });
                      respond({ accepted: false });
                    }
                  }}
                >
                  拒绝更改
                </Button>
                <Button
                  disabled={status !== "executing"}
                  onClick={() => {
                    if (respond && editorInstance) {
                      setAccepted(true);
                      editorInstance.commands.setContent(
                        fromMarkdown(agentState?.document || "")
                      );
                      setCurrentDocument(agentState?.document || "");
                      setAgentState({ document: agentState?.document || "" });
                      respond({ accepted: true });
                    }
                  }}
                >
                  <CheckCircle2 className="w-4 h-4 mr-2" />
                  接受更改
                </Button>
              </div>
            )}
            
            {accepted !== null && (
              <div className="mt-6 flex justify-end">
                <Badge variant={accepted ? "default" : "secondary"}>
                  {accepted ? (
                    <>
                      <CheckCircle2 className="w-3 h-3 mr-1" />
                      已接受
                    </>
                  ) : (
                    <>
                      <AlertCircle className="w-3 h-3 mr-1" />
                      已拒绝
                    </>
                  )}
                </Badge>
              </div>
            )}
          </CardContent>
        </Card>
      );
    },
  });

  useCopilotChatSuggestions({
    instructions: chatSuggestions.sample_agent,
  });

  const defaultContent = {
    type: "doc",
    content: [
      {
        type: "paragraph",
        content: [
          {
            type: "text",
            text: "",
          },
        ],
      },
    ],
  };

  return (
    <Card className="h-full border-0 shadow-none">
      <CardContent className="p-0 h-full">
        <div className="relative h-full">
          {/* 保存状态指示器 */}
          <div className="absolute top-4 right-4 z-10">
            <Badge variant="secondary" className="text-xs">
              {saveStatus === "已保存" ? (
                <>
                  <CheckCircle2 className="w-3 h-3 mr-1 text-green-500" />
                  {saveStatus}
                </>
              ) : (
                <>
                  <Timer className="w-3 h-3 mr-1 text-yellow-500" />
                  {saveStatus}
                </>
              )}
            </Badge>
          </div>

          {/* AI加载指示器 */}
          {isLoading && (
            <div className="absolute top-4 left-4 z-10">
              <Badge variant="outline" className="text-xs">
                <Wand2 className="w-3 h-3 mr-1 animate-pulse text-blue-500" />
                AI正在处理...
              </Badge>
            </div>
          )}

          {/* Novel编辑器 */}
          <div className="h-full overflow-hidden">
            <EditorRoot>
              <NovelEditorContent
                immediatelyRender={false}
                initialContent={defaultContent as JSONContent}
                extensions={extensions}
                className="h-full border-none focus:outline-none"
                editorProps={{
                  handleDOMEvents: {
                    keydown: (_view, event) => handleCommandNavigation(event),
                  },
                  handlePaste: (view, event) =>
                    handleImagePaste(view, event, uploadFn),
                  handleDrop: (view, event, _slice, moved) =>
                    handleImageDrop(view, event, moved, uploadFn),
                  attributes: {
                    class: "prose prose-lg dark:prose-invert max-w-none focus:outline-none px-8 py-12 min-h-full",
                  },
                }}
                onUpdate={({ editor }) => {
                  setEditorInstance(editor);
                  debouncedUpdates(editor);
                  setSaveStatus("保存中...");
                }}
                onCreate={({ editor }) => {
                  setEditorInstance(editor);
                }}
                slotAfter={<ImageResizer />}
              >
                <EditorCommand className="z-50 h-auto max-h-[330px] overflow-y-auto rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-1 py-2 shadow-xl">
                  <EditorCommandEmpty className="px-2 text-gray-500 dark:text-gray-400">
                    未找到结果
                  </EditorCommandEmpty>
                  <EditorCommandList>
                    {suggestionItems.map((item) => (
                      <EditorCommandItem
                        value={item.title}
                        onCommand={(val) => item.command?.(val)}
                        className="flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 aria-selected:bg-gray-100 dark:aria-selected:bg-gray-700"
                        key={item.title}
                      >
                        <div className="flex h-10 w-10 items-center justify-center rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700">
                          {item.icon}
                        </div>
                        <div>
                          <p className="font-medium text-gray-900 dark:text-gray-100">
                            {item.title}
                          </p>
                          <p className="text-xs text-gray-500 dark:text-gray-400">
                            {item.description}
                          </p>
                        </div>
                      </EditorCommandItem>
                    ))}
                  </EditorCommandList>
                </EditorCommand>

                {/* AI增强的悬浮工具栏 - 选中文字时显示,包含润色、扩写等功能 */}
                <GenerativeMenuSwitch open={openAI} onOpenChange={setOpenAI}>
                  <Separator orientation="vertical" />
                  <NodeSelector open={openNode} onOpenChange={setOpenNode} />
                  <Separator orientation="vertical" />
                  <TextButtons />
                  <Separator orientation="vertical" />
                  <ColorSelector open={openColor} onOpenChange={setOpenColor} />
                  <Separator orientation="vertical" />
                  <LinkSelector open={openLink} onOpenChange={setOpenLink} />
                  <Separator orientation="vertical" />
                  <MathSelector />
                </GenerativeMenuSwitch>
              </NovelEditorContent>
            </EditorRoot>
          </div>

          {/* 占位符 */}
          {placeholderVisible && (
            <div className="absolute top-20 left-12 pointer-events-none">
              <div className="text-gray-400 dark:text-gray-500 text-lg">
                ✨ 开始输入,让AI助手帮您创作精彩内容...
              </div>
              <div className="text-gray-300 dark:text-gray-600 text-sm mt-2">
                尝试输入 "/" 来查看可用的命令和格式选项
              </div>
              <div className="mt-4 space-y-2">
                <div className="flex items-center space-x-2 text-gray-300 dark:text-gray-600 text-xs">
                  <div className="w-4 h-4 bg-blue-100 dark:bg-blue-900 rounded flex items-center justify-center">
                    <Brain className="w-2 h-2 text-blue-600 dark:text-blue-400" />
                  </div>
                  <span>选中文字后点击"Ask AI"进行润色、扩写</span>
                </div>
                <div className="flex items-center space-x-2 text-gray-300 dark:text-gray-600 text-xs">
                  <div className="w-4 h-4 bg-green-100 dark:bg-green-900 rounded flex items-center justify-center">
                    <Wand2 className="w-2 h-2 text-green-600 dark:text-green-400" />
                  </div>
                  <span>右侧AI助手可以帮您优化文本、生成内容</span>
                </div>
                <div className="flex items-center space-x-2 text-gray-300 dark:text-gray-600 text-xs">
                  <div className="w-4 h-4 bg-purple-100 dark:bg-purple-900 rounded flex items-center justify-center">
                    <Sparkles className="w-2 h-2 text-purple-600 dark:text-purple-400" />
                  </div>
                  <span>支持markdown语法、代码块、数学公式</span>
                </div>
              </div>
            </div>
          )}
        </div>
      </CardContent>
    </Card>
  );
};

const StatusBar = () => {
  const [wordCount, setWordCount] = useState(0);
  const [readingTime, setReadingTime] = useState(0);
  
  return (
    <footer className="h-10 bg-white/90 dark:bg-gray-900/90 backdrop-blur-xl border-t border-gray-200/30 dark:border-gray-700/30 flex items-center justify-between px-6 text-xs text-gray-600 dark:text-gray-400 relative">
      {/* 顶部装饰线 */}
      <div className="absolute top-0 left-0 right-0 h-[1px] bg-gradient-to-r from-transparent via-cyan-500/50 to-transparent" />
      
      <div className="flex items-center space-x-6">
        <div className="flex items-center space-x-2 group hover:text-emerald-600 dark:hover:text-emerald-400 transition-colors duration-300">
          <div className="w-4 h-4 bg-gradient-to-r from-emerald-500 to-green-500 rounded-full flex items-center justify-center">
            <FileText className="w-2 h-2 text-white" />
          </div>
          <span className="font-medium">系统就绪</span>
        </div>
        
        <div className="flex items-center space-x-2 group hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-300">
          <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
          <CheckCircle2 className="w-3 h-3 text-emerald-500" />
          <span>AI助手在线</span>
        </div>
      </div>
      
      <div className="flex items-center space-x-6">
        <div className="flex items-center space-x-2 group hover:text-purple-600 dark:hover:text-purple-400 transition-colors duration-300">
          <Clock className="w-3 h-3" />
          <span>实时保存</span>
        </div>
        
        <div className="flex items-center space-x-2 group hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-300">
          <div className="w-4 h-4 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full flex items-center justify-center">
            <Sparkles className="w-2 h-2 text-white" />
          </div>
          <span className="font-medium">Novel编辑器</span>
        </div>
        
        <div className="flex items-center space-x-2 text-xs bg-gradient-to-r from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-700 px-2 py-1 rounded-full">
          <span>v1.0.0</span>
        </div>
      </div>
    </footer>
  );
};

function fromMarkdown(text: string) {
  const md = new MarkdownIt({
    typographer: true,
    html: true,
  });
  return md.render(text);
}

function diffPartialText(
  oldText: string,
  newText: string,
  isComplete: boolean = false
) {
  let oldTextToCompare = oldText;
  if (oldText.length > newText.length && !isComplete) {
    oldTextToCompare = oldText.slice(0, newText.length);
  }

  const changes = diffWords(oldTextToCompare, newText);
  let result = "";
  
  changes.forEach((part) => {
    if (part.added) {
      result += `<em>${part.value}</em>`;
    } else if (part.removed) {
      result += `<s>${part.value}</s>`;
    } else {
      result += part.value;
    }
  });

  if (oldText.length > newText.length && !isComplete) {
    result += oldText.slice(newText.length);
  }
  
  return result;

style.css

/* 现代化AI文档编辑器样式 */

/* 全局布局样式 */
.document-editor-app {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* 炫酷动画效果 */
@keyframes gradient-shift {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}

@keyframes float {
  0%, 100% { transform: translateY(0px); }
  50% { transform: translateY(-10px); }
}

@keyframes pulse-glow {
  0%, 100% { box-shadow: 0 0 5px rgba(168, 85, 247, 0.4); }
  50% { box-shadow: 0 0 20px rgba(168, 85, 247, 0.8), 0 0 30px rgba(168, 85, 247, 0.4); }
}

@keyframes shimmer {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

@keyframes bounce-gentle {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-2px); }
}

@keyframes rotate-slow {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

@keyframes scale-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.05); }
}

/* 动画延迟类 */
.animation-delay-1000 {
  animation-delay: 1s;
}

.animation-delay-2000 {
  animation-delay: 2s;
}

.animation-delay-3000 {
  animation-delay: 3s;
}

/* 炫酷效果类 */
.gradient-bg {
  background: linear-gradient(-45deg, #667eea, #764ba2, #667eea, #764ba2);
  background-size: 400% 400%;
  animation: gradient-shift 3s ease infinite;
}

.float-animation {
  animation: float 3s ease-in-out infinite;
}

.pulse-glow {
  animation: pulse-glow 2s ease-in-out infinite;
}

.shimmer-effect::before {
  content: '';
  position: absolute;
  top: 0;
  left: -100%;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
  animation: shimmer 2s infinite;
}

.bounce-gentle {
  animation: bounce-gentle 2s ease-in-out infinite;
}

.rotate-slow {
  animation: rotate-slow 10s linear infinite;
}

.scale-pulse {
  animation: scale-pulse 2s ease-in-out infinite;
}

/* 顶部导航栏样式 */
.top-navigation {
  backdrop-filter: blur(20px);
  background: rgba(255, 255, 255, 0.9);
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.dark .top-navigation {
  background: rgba(17, 24, 39, 0.9);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

/* 编辑器工具栏样式 */
.editor-toolbar {
  backdrop-filter: blur(20px);
  background: rgba(255, 255, 255, 0.95);
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}

.dark .editor-toolbar {
  background: rgba(17, 24, 39, 0.95);
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}

/* 编辑器内容样式 */
.ProseMirror {
  outline: none;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: 16px;
  line-height: 1.75;
  color: #1f2937;
  background: transparent;
  min-height: 100%;
  padding: 2rem 0;
  transition: all 0.3s ease;
}

.dark .ProseMirror {
  color: #f9fafb;
}

/* 悬浮效果 */
.hover-lift {
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.hover-lift:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}

/* 渐变按钮效果 */
.gradient-button {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  position: relative;
  overflow: hidden;
  transition: all 0.3s ease;
}

.gradient-button::before {
  content: '';
  position: absolute;
  top: 0;
  left: -100%;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
  transition: left 0.5s;
}

.gradient-button:hover::before {
  left: 100%;
}

/* 玻璃态效果 */
.glass-effect {
  background: rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.2);
  box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
}

.dark .glass-effect {
  background: rgba(17, 24, 39, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.1);
}

/* 荧光效果 */
.neon-glow {
  box-shadow: 
    0 0 5px rgba(168, 85, 247, 0.5),
    0 0 10px rgba(168, 85, 247, 0.3),
    0 0 15px rgba(168, 85, 247, 0.2),
    0 0 20px rgba(168, 85, 247, 0.1);
}

.dark .neon-glow {
  box-shadow: 
    0 0 5px rgba(168, 85, 247, 0.7),
    0 0 10px rgba(168, 85, 247, 0.5),
    0 0 15px rgba(168, 85, 247, 0.3),
    0 0 20px rgba(168, 85, 247, 0.2);
}

/* 占位符样式 */
.ProseMirror .is-editor-empty:first-child::before {
  content: attr(data-placeholder);
  float: left;
  color: #9ca3af;
  pointer-events: none;
  height: 0;
  font-style: italic;
  opacity: 0;
  animation: fadeIn 0.5s ease-in-out 0.5s forwards;
}

.dark .ProseMirror .is-editor-empty:first-child::before {
  color: #6b7280;
}

/* 文本渐变效果 */
.text-gradient {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.dark .text-gradient {
  background: linear-gradient(135deg, #9333ea 0%, #3b82f6 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

/* 标题样式 */
.ProseMirror h1 {
  font-size: 2.25rem;
  font-weight: 700;
  line-height: 1.2;
  margin-top: 2rem;
  margin-bottom: 1rem;
  color: #111827;
  background: linear-gradient(135deg, #1f2937 0%, #4f46e5 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.dark .ProseMirror h1 {
  background: linear-gradient(135deg, #f9fafb 0%, #60a5fa 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.ProseMirror h2 {
  font-size: 1.875rem;
  font-weight: 600;
  line-height: 1.3;
  margin-top: 1.5rem;
  margin-bottom: 0.75rem;
  color: #1f2937;
}

.dark .ProseMirror h2 {
  color: #f3f4f6;
}

.ProseMirror h3 {
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1.4;
  margin-top: 1.25rem;
  margin-bottom: 0.5rem;
  color: #1f2937;
}

.dark .ProseMirror h3 {
  color: #f3f4f6;
}

/* 段落样式 */
.ProseMirror p {
  margin-top: 0;
  margin-bottom: 1.25rem;
  line-height: 1.75;
}

.ProseMirror p:last-child {
  margin-bottom: 0;
}

/* 列表样式 */
.ProseMirror ul,
.ProseMirror ol {
  margin-top: 0;
  margin-bottom: 1.25rem;
  padding-left: 1.5rem;
}

.ProseMirror ul li,
.ProseMirror ol li {
  margin-bottom: 0.5rem;
}

.ProseMirror ul li::marker {
  color: #6b7280;
}

.ProseMirror ol li::marker {
  color: #6b7280;
  font-weight: 500;
}

/* 引用样式 */
.ProseMirror blockquote {
  margin: 1.5rem 0;
  padding-left: 1.5rem;
  border-left: 4px solid;
  border-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%) 1;
  color: #6b7280;
  font-style: italic;
  position: relative;
}

.ProseMirror blockquote::before {
  content: '';
  position: absolute;
  left: -4px;
  top: 0;
  bottom: 0;
  width: 4px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.dark .ProseMirror blockquote {
  color: #9ca3af;
}

/* 代码样式 */
.ProseMirror code {
  background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
  color: #e11d48;
  padding: 0.125rem 0.25rem;
  border-radius: 0.25rem;
  font-size: 0.875em;
  font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
  border: 1px solid rgba(0, 0, 0, 0.1);
}

.dark .ProseMirror code {
  background: linear-gradient(135deg, #374151 0%, #4b5563 100%);
  color: #fbbf24;
  border: 1px solid rgba(255, 255, 255, 0.1);
}

.ProseMirror pre {
  background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
  border: 1px solid #e2e8f0;
  border-radius: 0.75rem;
  padding: 1.5rem;
  margin: 1.5rem 0;
  overflow-x: auto;
  font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
  font-size: 0.875rem;
  line-height: 1.5;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}

.dark .ProseMirror pre {
  background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
  border-color: #4b5563;
}

.ProseMirror pre code {
  background: transparent;
  color: inherit;
  padding: 0;
  border-radius: 0;
  font-size: inherit;
  border: none;
}

/* 链接样式 */
.ProseMirror a {
  color: #2563eb;
  text-decoration: none;
  border-bottom: 2px solid transparent;
  background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  transition: all 0.3s ease;
  position: relative;
}

.ProseMirror a::after {
  content: '';
  position: absolute;
  bottom: -2px;
  left: 0;
  width: 0;
  height: 2px;
  background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
  transition: width 0.3s ease;
}

.ProseMirror a:hover::after {
  width: 100%;
}

.dark .ProseMirror a {
  background: linear-gradient(135deg, #60a5fa 0%, #93c5fd 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

/* 分割线样式 */
.ProseMirror hr {
  border: none;
  height: 2px;
  background: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%);
  margin: 2rem 0;
  border-radius: 1px;
}

.dark .ProseMirror hr {
  background: linear-gradient(135deg, #4b5563 0%, #6b7280 100%);
}

/* 表格样式 */
.ProseMirror table {
  border-collapse: collapse;
  margin: 1.5rem 0;
  width: 100%;
  background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
  border-radius: 0.5rem;
  overflow: hidden;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}

.ProseMirror table td,
.ProseMirror table th {
  border: 1px solid #e5e7eb;
  padding: 0.75rem;
  text-align: left;
}

.ProseMirror table th {
  background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
  font-weight: 600;
}

.dark .ProseMirror table {
  background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
}

.dark .ProseMirror table td,
.dark .ProseMirror table th {
  border-color: #4b5563;
}

.dark .ProseMirror table th {
  background: linear-gradient(135deg, #374151 0%, #4b5563 100%);
}

/* 任务列表样式 */
.ProseMirror ul[data-type="taskList"] {
  list-style: none;
  padding-left: 0;
}

.ProseMirror ul[data-type="taskList"] li {
  display: flex;
  align-items: flex-start;
  margin-bottom: 0.5rem;
}

.ProseMirror ul[data-type="taskList"] li input[type="checkbox"] {
  margin-right: 0.5rem;
  margin-top: 0.125rem;
  flex-shrink: 0;
  accent-color: #667eea;
}

/* 图片样式 */
.ProseMirror img {
  max-width: 100%;
  height: auto;
  border-radius: 0.75rem;
  box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.ProseMirror img:hover {
  transform: scale(1.02);
  box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.15);
}

/* 选中状态样式 */
.ProseMirror-selectednode {
  outline: 2px solid #667eea;
  outline-offset: 2px;
  border-radius: 0.25rem;
}

/* AI高亮样式 */
.ProseMirror .ai-highlight {
  background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
  padding: 0.125rem 0.25rem;
  border-radius: 0.25rem;
  animation: highlight-pulse 2s ease-in-out infinite;
  border: 1px solid rgba(251, 191, 36, 0.3);
}

.dark .ProseMirror .ai-highlight {
  background: linear-gradient(135deg, #451a03 0%, #78350f 100%);
  border: 1px solid rgba(251, 191, 36, 0.2);
}

@keyframes highlight-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.8; }
}

/* 斜杠命令菜单样式 */
.slash-command-menu {
  backdrop-filter: blur(20px);
  background: rgba(255, 255, 255, 0.95);
  border: 1px solid rgba(0, 0, 0, 0.1);
  box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.1);
  border-radius: 1rem;
}

.dark .slash-command-menu {
  background: rgba(17, 24, 39, 0.95);
  border-color: rgba(255, 255, 255, 0.1);
}

/* 悬浮工具栏样式 */
.floating-toolbar {
  backdrop-filter: blur(20px);
  background: rgba(255, 255, 255, 0.95);
  border: 1px solid rgba(0, 0, 0, 0.1);
  box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.1);
  border-radius: 0.75rem;
}

.dark .floating-toolbar {
  background: rgba(17, 24, 39, 0.95);
  border-color: rgba(255, 255, 255, 0.1);
}

/* 状态栏样式 */
.status-bar {
  backdrop-filter: blur(20px);
  background: rgba(249, 250, 251, 0.95);
  border-top: 1px solid rgba(0, 0, 0, 0.06);
}

.dark .status-bar {
  background: rgba(17, 24, 39, 0.95);
  border-top-color: rgba(255, 255, 255, 0.06);
}

/* 自定义滚动条 */
.custom-scrollbar::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}

.custom-scrollbar::-webkit-scrollbar-track {
  background: transparent;
}

.custom-scrollbar::-webkit-scrollbar-thumb {
  background: linear-gradient(135deg, #d1d5db 0%, #9ca3af 100%);
  border-radius: 3px;
}

.custom-scrollbar::-webkit-scrollbar-thumb:hover {
  background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%);
}

.dark .custom-scrollbar::-webkit-scrollbar-thumb {
  background: linear-gradient(135deg, #4b5563 0%, #6b7280 100%);
}

.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover {
  background: linear-gradient(135deg, #6b7280 0%, #9ca3af 100%);
}

/* 动画效果 */
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes slideIn {
  from { transform: translateX(20px); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}

@keyframes scaleIn {
  from { transform: scale(0.9); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

.fade-in {
  animation: fadeIn 0.5s ease-out;
}

.slide-in {
  animation: slideIn 0.5s ease-out;
}

.scale-in {
  animation: scaleIn 0.3s ease-out;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .ProseMirror {
    font-size: 14px;
    padding: 1rem 0;
  }
  
  .ProseMirror h1 {
    font-size: 1.875rem;
  }
  
  .ProseMirror h2 {
    font-size: 1.5rem;
  }
  
  .ProseMirror h3 {
    font-size: 1.25rem;
  }
}

/* CopilotKit自定义样式 */
.copilotKitWindow {
  border-radius: 1rem !important;
  box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.1) !important;
  border: 1px solid rgba(0, 0, 0, 0.1) !important;
  backdrop-filter: blur(20px) !important;
}

.dark .copilotKitWindow {
  border-color: rgba(255, 255, 255, 0.1) !important;
}

.copilotKitHeader {
  background: rgba(249, 250, 251, 0.95) !important;
  backdrop-filter: blur(20px) !important;
  border-bottom: 1px solid rgba(0, 0, 0, 0.06) !important;
}

.dark .copilotKitHeader {
  background: rgba(17, 24, 39, 0.95) !important;
  border-bottom-color: rgba(255, 255, 255, 0.06) !important;
}

/* 高质量排版 */
.ProseMirror {
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* 打印样式 */
@media print {
  .ProseMirror {
    font-size: 12pt;
    line-height: 1.5;
    color: black;
  }
  
  .ProseMirror h1 {
    font-size: 18pt;
  }
  
  .ProseMirror h2 {
    font-size: 16pt;
  }
  
  .ProseMirror h3 {
    font-size: 14pt;
  }
}

command

// SPDX-License-Identifier: MIT

import {
  ArrowDownWideNarrow,
  CheckCheck,
  RefreshCcwDot,
  StepForward,
  WrapText,
  Sparkles,
  Zap,
  PenTool,
} from "lucide-react";
import { getPrevText, useEditor } from "novel";
import { CommandGroup, CommandItem, CommandSeparator } from "../../ui/command";
import { Button } from "../../ui/button";

const options = [
  {
    value: "improve",
    label: "优化文本",
    description: "改进写作质量和表达",
    icon: RefreshCcwDot,
    gradient: "from-emerald-500 to-teal-500",
    bgGradient: "from-emerald-50 to-teal-50 dark:from-emerald-900/20 dark:to-teal-900/20",
  },
  {
    value: "fix",
    label: "语法修正",
    description: "检查并修复语法错误",
    icon: CheckCheck,
    gradient: "from-blue-500 to-cyan-500",
    bgGradient: "from-blue-50 to-cyan-50 dark:from-blue-900/20 dark:to-cyan-900/20",
  },
  {
    value: "shorter",
    label: "内容精简",
    description: "保持核心意思,减少冗余",
    icon: ArrowDownWideNarrow,
    gradient: "from-orange-500 to-red-500",
    bgGradient: "from-orange-50 to-red-50 dark:from-orange-900/20 dark:to-red-900/20",
  },
  {
    value: "longer",
    label: "内容扩写",
    description: "增加细节和详细描述",
    icon: WrapText,
    gradient: "from-purple-500 to-pink-500",
    bgGradient: "from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20",
  },
  {
    value: "creative",
    label: "创意改写",
    description: "用更有创意的方式表达",
    icon: Sparkles,
    gradient: "from-violet-500 to-purple-500",
    bgGradient: "from-violet-50 to-purple-50 dark:from-violet-900/20 dark:to-purple-900/20",
  },
  {
    value: "professional",
    label: "专业化",
    description: "转换为更正式的表达",
    icon: PenTool,
    gradient: "from-gray-600 to-gray-800",
    bgGradient: "from-gray-50 to-gray-100 dark:from-gray-800/20 dark:to-gray-900/20",
  },
];

interface AISelectorCommandsProps {
  onSelect: (value: string, option: string) => void;
}

const AISelectorCommands = ({ onSelect }: AISelectorCommandsProps) => {
  const { editor } = useEditor();

  if (!editor) return null;

  return (
    <div className="space-y-3">
      <div className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider px-1">
        AI 功能选项
      </div>
      
      <div className="grid grid-cols-1 gap-2">
        {options.map((option, index) => {
          const Icon = option.icon;
          return (
            <Button
              key={option.value}
              variant="ghost"
              className={`group relative overflow-hidden h-auto p-0 bg-gradient-to-r ${option.bgGradient} border border-gray-200/50 dark:border-gray-700/50 hover:border-gray-300 dark:hover:border-gray-600 rounded-xl transition-all duration-300 hover:shadow-lg hover:scale-[1.02]`}
              onClick={() => onSelect(getPrevText(editor, 5000), option.value)}
            >
              <div className="relative w-full p-4 flex items-center space-x-3">
                {/* 图标容器 */}
                <div className={`w-10 h-10 bg-gradient-to-r ${option.gradient} rounded-lg flex items-center justify-center shadow-lg group-hover:shadow-xl transition-all duration-300 group-hover:scale-110`}>
                  <Icon className="w-5 h-5 text-white" />
                </div>
                
                {/* 文本内容 */}
                <div className="flex-1 text-left">
                  <div className="font-medium text-gray-900 dark:text-white group-hover:text-gray-700 dark:group-hover:text-gray-200 transition-colors">
                    {option.label}
                  </div>
                  <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
                    {option.description}
                  </div>
                </div>
                
                {/* 箭头指示器 */}
                <div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
                  <StepForward className="w-4 h-4 text-gray-400" />
                </div>
                
                {/* 悬停效果背景 */}
                <div className="absolute inset-0 bg-white/50 dark:bg-gray-800/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-xl" />
              </div>
            </Button>
          );
        })}
      </div>
      
      {/* 快捷提示 */}
      <div className="mt-4 p-3 bg-gradient-to-r from-blue-50 via-purple-50 to-pink-50 dark:from-blue-900/10 dark:via-purple-900/10 dark:to-pink-900/10 rounded-xl border border-blue-200/30 dark:border-blue-700/30">
        <div className="flex items-center space-x-2">
          <Zap className="w-4 h-4 text-blue-500" />
          <span className="text-xs text-gray-600 dark:text-gray-400">
            选择一个选项或在上方输入自定义指令
          </span>
        </div>
      </div>
    </div>
  );
};

export default AISelectorCommands;

ai-selector

// SPDX-License-Identifier: MIT

"use client";

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

import { ArrowUp } from "lucide-react";
import { useEditor } from "novel";
import { addAIHighlight } from "novel";
import { useCallback, useState } from "react";
import Markdown from "react-markdown";
import { toast } from "sonner";
import { Button } from "../../ui/button";
import Magic from "../../ui/icons/magic";
import { ScrollArea } from "../../ui/scroll-area";
import AICompletionCommands from "./ai-completion-command";
import AISelectorCommands from "./ai-selector-commands";
import { LoadingOutlined } from "@ant-design/icons";
// import { resolveServiceURL } from "~/core/api/resolve-service-url";
// import { fetchStream } from "~/core/sse";
//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;
}

// 临时模拟函数,后续需要替换为实际实现
const fetchStream = async (url: string, options: RequestInit) => {
  return [] as any;
};

const resolveServiceURL = (path: string) => {
  return path;
};

function useProseCompletion() {
  const [completion, setCompletion] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  const complete = useCallback(
    async (prompt: string, options?: { body?: Record<string, any> }) => {
      setIsLoading(true);
      setError(null);

      try {
        const response = await fetchStream(
          resolveServiceURL("/api/prose/generate"),
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              prompt,
              ...options?.body,
            }),
          },
        );

        let fullText = "";

        // Process the streaming response
        for await (const chunk of response) {
          fullText += chunk.data;
          setCompletion(fullText);
        }

        setIsLoading(false);
        return fullText;
      } catch (e) {
        const error = e instanceof Error ? e : new Error("An error occurred");
        setError(error);
        toast.error(error.message);
        setIsLoading(false);
        throw error;
      }
    },
    [],
  );

  const reset = useCallback(() => {
    setCompletion("");
    setError(null);
    setIsLoading(false);
  }, []);

  return {
    completion,
    // complete,
    isLoading,
    error,
    reset,
  };
}

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

  const { completion, isLoading } = useProseCompletion();

  if (!editor) return null;

  const hasCompletion = completion.length > 0;

  return (
    <div className="w-[420px] bg-white/95 dark:bg-gray-900/95 backdrop-blur-xl border border-gray-200/50 dark:border-gray-700/50 rounded-2xl shadow-2xl overflow-hidden">
      {/* 渐变标题栏 */}
      <div className="bg-gradient-to-r from-purple-500 via-blue-500 to-cyan-500 p-[1px]">
        <div className="bg-white/95 dark:bg-gray-900/95 backdrop-blur-xl rounded-t-2xl p-4">
          <div className="flex items-center space-x-3">
            <div className="w-8 h-8 bg-gradient-to-r from-purple-500 to-blue-500 rounded-lg flex items-center justify-center">
              <Magic className="w-4 h-4 text-white" />
            </div>
            <div>
              <h3 className="font-semibold text-gray-900 dark:text-white">AI 写作助手</h3>
              <p className="text-xs text-gray-500 dark:text-gray-400">
                {hasCompletion ? "处理您的内容" : "选择或输入 AI 指令"}
              </p>
            </div>
          </div>
        </div>
      </div>

      {/* 内容区域 */}
      <div className="p-6">
        {hasCompletion && (
          <div className="mb-6 max-h-[300px] overflow-hidden">
            <div className="bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-xl p-4 border border-blue-200/50 dark:border-blue-700/50">
              <ScrollArea className="max-h-[250px]">
                <div className="prose prose-sm dark:prose-invert prose-blue">
                  <Markdown>{completion}</Markdown>
                </div>
              </ScrollArea>
            </div>
          </div>
        )}

        {isLoading && (
          <div className="flex items-center justify-center p-8">
            <div className="relative">
              <div className="w-12 h-12 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full flex items-center justify-center animate-pulse">
                <Magic className="w-6 h-6 text-white" />
              </div>
              <div className="absolute inset-0 w-12 h-12 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full animate-ping opacity-30"></div>
            </div>
            <div className="ml-4">
              <p className="font-medium text-gray-900 dark:text-white">AI 正在思考...</p>
              <p className="text-sm text-gray-500 dark:text-gray-400">请稍等片刻</p>
            </div>
          </div>
        )}

        {!isLoading && (
          <div className="space-y-4">
            {/* 输入框 */}
            <div className="relative group">
              <div className="absolute inset-0 bg-gradient-to-r from-purple-500/20 to-blue-500/20 rounded-xl blur-sm group-focus-within:blur-none transition-all duration-300"></div>
              <div className="relative bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 group-focus-within:border-purple-500/50 dark:group-focus-within:border-purple-400/50 transition-all duration-300">
                <CommandInput
                  value={inputValue}
                  onValueChange={setInputValue}
                  autoFocus
                  placeholder={
                    hasCompletion
                      ? "告诉 AI 下一步要做什么..."
                      : "请输入您的指令或选择下方选项..."
                  }
                  onFocus={() => addAIHighlight(editor)}
                  className="border-0 bg-transparent px-4 py-3 text-gray-900 dark:text-white placeholder:text-gray-500 dark:placeholder:text-gray-400 focus:outline-none focus:ring-0"
                />
                <Button
                  size="sm"
                  className="absolute top-2 right-2 h-8 w-8 bg-gradient-to-r from-purple-500 to-blue-500 hover:from-purple-600 hover:to-blue-600 border-0 shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200"
                >
                  <ArrowUp className="h-4 w-4 text-white" />
                </Button>
              </div>
            </div>

            {/* AI 选项 */}
            {hasCompletion ? (
              <AICompletionCommands
                onDiscard={() => {
                  editor.chain().unsetHighlight().focus().run();
                  onOpenChange(false);
                }}
                completion={completion}
              />
            ) : (
              <div className="space-y-2">
                <AISelectorCommands
                  onSelect={(value, option) => {
                    // complete(value, { body: { option } })
                  }}
                />
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

generative-menu-switch

// SPDX-License-Identifier: MIT

import { EditorBubble, removeAIHighlight, useEditor } from "novel";
import { Fragment, type ReactNode, useEffect } from "react";
import { Button } from "../../ui/button";
import Magic from "../../ui/icons/magic";
import { AISelector } from "./ai-selector";
import { useReplay } from "@/app/core/replay";
import { TooltipContent, TooltipTrigger, Tooltip } from "@/components/ui/tooltip";
import { Sparkles, Wand2, Stars } from "lucide-react";

interface GenerativeMenuSwitchProps {
  children: ReactNode;
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

const GenerativeMenuSwitch = ({
  children,
  open,
  onOpenChange,
}: GenerativeMenuSwitchProps) => {
  const { editor } = useEditor();
  const { isReplay } = useReplay();
  
  useEffect(() => {
    if (!open && editor) removeAIHighlight(editor);
  }, [open]);

  if (!editor) return null;
  
  return (
    <EditorBubble
      tippyOptions={{
        placement: open ? "bottom-start" : "top",
        onHidden: () => {
          onOpenChange(false);
          editor.chain().unsetHighlight().run();
        },
      }}
      className="border-none bg-transparent shadow-none"
    >
      {open && <AISelector open={open} onOpenChange={onOpenChange} />}
      {!open && (
        <div className="bg-white/95 dark:bg-gray-900/95 backdrop-blur-xl border border-gray-200/50 dark:border-gray-700/50 rounded-2xl shadow-2xl overflow-hidden">
          {/* 渐变边框效果 */}
          <div className="bg-gradient-to-r from-purple-500 via-blue-500 to-cyan-500 p-[1px] rounded-2xl">
            <div className="bg-white dark:bg-gray-900 rounded-2xl p-1">
              <div className="flex items-center">
                {isReplay ? (
                  <Tooltip>
                    <TooltipTrigger>
                      <Button
                        className="relative overflow-hidden bg-gradient-to-r from-gray-400 to-gray-500 text-white border-0 rounded-xl px-4 py-2 shadow-lg cursor-not-allowed"
                        variant="ghost"
                        size="sm"
                        disabled
                      >
                        <div className="flex items-center space-x-2">
                          <div className="w-5 h-5 bg-white/20 rounded-lg flex items-center justify-center">
                            <Magic className="h-3 w-3" />
                          </div>
                          <span className="font-medium">Ask AI</span>
                        </div>
                      </Button>
                    </TooltipTrigger>
                    <TooltipContent>回放模式下无法使用AI功能</TooltipContent>
                  </Tooltip>
                ) : (
                  <Button
                    className="relative overflow-hidden bg-gradient-to-r from-purple-500 via-blue-500 to-cyan-500 hover:from-purple-600 hover:via-blue-600 hover:to-cyan-600 text-white border-0 rounded-xl px-4 py-2 shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-300 group"
                    variant="ghost"
                    onClick={() => onOpenChange(true)}
                    size="sm"
                  >
                    {/* 背景动画效果 */}
                    <div className="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
                    
                    <div className="relative flex items-center space-x-2">
                      {/* 图标容器 */}
                      <div className="w-5 h-5 bg-white/20 rounded-lg flex items-center justify-center group-hover:bg-white/30 transition-colors duration-300">
                        <Magic className="h-3 w-3 group-hover:scale-110 transition-transform duration-300" />
                      </div>
                      <span className="font-medium">Ask AI</span>
                      
                      {/* 闪烁星星效果 */}
                      <div className="absolute -top-1 -right-1 opacity-60 group-hover:opacity-100 transition-opacity duration-300">
                        <Sparkles className="w-3 h-3 animate-pulse" />
                      </div>
                    </div>
                    
                    {/* 光泽效果 */}
                    <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700 ease-in-out" />
                  </Button>
                )}
                
                {/* 分隔线 */}
                <div className="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-2" />
                
                {/* 其他工具 */}
                <div className="flex items-center space-x-1">
                  {children}
                </div>
              </div>
            </div>
          </div>
        </div>
      )}
    </EditorBubble>
  );
};

export default GenerativeMenuSwitch;