mirror of
				https://litchi.icu/ngc2207/judge.git
				synced 2025-11-04 06:03:43 +00:00 
			
		
		
		
	feat: add tooltip, button components and editor utility functions for enhanced code editing experience
This commit is contained in:
		
							parent
							
								
									1aeb472495
								
							
						
					
					
						commit
						3587cfe009
					
				
										
											Binary file not shown.
										
									
								
							@ -13,7 +13,9 @@
 | 
			
		||||
    "@monaco-editor/react": "^4.6.0",
 | 
			
		||||
    "@radix-ui/react-scroll-area": "^1.2.2",
 | 
			
		||||
    "@radix-ui/react-select": "^2.1.4",
 | 
			
		||||
    "@radix-ui/react-slot": "^1.1.1",
 | 
			
		||||
    "@radix-ui/react-tabs": "^1.1.2",
 | 
			
		||||
    "@radix-ui/react-tooltip": "^1.1.6",
 | 
			
		||||
    "class-variance-authority": "^0.7.1",
 | 
			
		||||
    "clsx": "^2.1.1",
 | 
			
		||||
    "devicons-react": "^1.4.0",
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,14 @@ import {
 | 
			
		||||
  SelectTrigger,
 | 
			
		||||
  SelectValue,
 | 
			
		||||
} from "@/components/ui/select";
 | 
			
		||||
import { Palette } from "lucide-react";
 | 
			
		||||
import {
 | 
			
		||||
  Copy,
 | 
			
		||||
  FileDown,
 | 
			
		||||
  Paintbrush,
 | 
			
		||||
  Palette,
 | 
			
		||||
  Redo2,
 | 
			
		||||
  Undo2,
 | 
			
		||||
} from "lucide-react";
 | 
			
		||||
import * as monaco from "monaco-editor";
 | 
			
		||||
import { highlighter } from "@/lib/shiki";
 | 
			
		||||
import { Bot, CodeXml } from "lucide-react";
 | 
			
		||||
@ -23,6 +30,21 @@ import { SUPPORTED_EDITOR_LANGUAGES_CONFIG } from "@/constants/languages";
 | 
			
		||||
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
 | 
			
		||||
import { connectToLanguageServer, SUPPORTED_LSP_LANGUAGES } from "@/lib/lsp";
 | 
			
		||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import {
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  TooltipContent,
 | 
			
		||||
  TooltipProvider,
 | 
			
		||||
  TooltipTrigger,
 | 
			
		||||
} from "@/components/ui/tooltip";
 | 
			
		||||
import {
 | 
			
		||||
  redoChange,
 | 
			
		||||
  undoChange,
 | 
			
		||||
  formatCode,
 | 
			
		||||
  downloadCode,
 | 
			
		||||
  copyCode,
 | 
			
		||||
  moveCursorToEnd,
 | 
			
		||||
} from "@/lib/editor";
 | 
			
		||||
 | 
			
		||||
self.MonacoEnvironment = {
 | 
			
		||||
  getWorker(_, _label) {
 | 
			
		||||
@ -37,23 +59,42 @@ function App() {
 | 
			
		||||
  const [theme, setTheme] = useState(DEFAULT_EDITOR_THEME);
 | 
			
		||||
  const file = DEFAULT_FILES[language];
 | 
			
		||||
  const webSocketRef = useRef<WebSocket | null>(null);
 | 
			
		||||
  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const closeWebSocket = () => {
 | 
			
		||||
      if (webSocketRef.current) {
 | 
			
		||||
        if (
 | 
			
		||||
          webSocketRef.current.readyState === WebSocket.OPEN ||
 | 
			
		||||
          webSocketRef.current.readyState === WebSocket.CONNECTING
 | 
			
		||||
        ) {
 | 
			
		||||
          webSocketRef.current.onclose = () => {
 | 
			
		||||
            webSocketRef.current = null;
 | 
			
		||||
          };
 | 
			
		||||
          webSocketRef.current.close();
 | 
			
		||||
        } else {
 | 
			
		||||
          webSocketRef.current = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    if (language in SUPPORTED_LSP_LANGUAGES) {
 | 
			
		||||
      connectToLanguageServer(language).then((webSocket) => {
 | 
			
		||||
        webSocketRef.current = webSocket;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (webSocketRef.current) {
 | 
			
		||||
        webSocketRef.current.close();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const connectWebSocket = async () => {
 | 
			
		||||
      closeWebSocket();
 | 
			
		||||
 | 
			
		||||
      if (editorRef.current && language in SUPPORTED_LSP_LANGUAGES) {
 | 
			
		||||
        try {
 | 
			
		||||
          const webSocket = await connectToLanguageServer(language);
 | 
			
		||||
          webSocket.onopen = () => {
 | 
			
		||||
            webSocketRef.current = webSocket;
 | 
			
		||||
          };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          console.error("Failed to connect to language server", error);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    connectWebSocket();
 | 
			
		||||
    return closeWebSocket;
 | 
			
		||||
  }, [language]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
@ -74,18 +115,6 @@ function App() {
 | 
			
		||||
                />
 | 
			
		||||
                代码
 | 
			
		||||
              </TabsTrigger>
 | 
			
		||||
              <TabsTrigger
 | 
			
		||||
                value="diff-editor"
 | 
			
		||||
                className="500 rounded-full data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=active]:shadow-none"
 | 
			
		||||
              >
 | 
			
		||||
                <Bot
 | 
			
		||||
                  className="-ms-0.5 me-1.5 opacity-60"
 | 
			
		||||
                  size={16}
 | 
			
		||||
                  strokeWidth={2}
 | 
			
		||||
                  aria-hidden="true"
 | 
			
		||||
                />
 | 
			
		||||
                AI 助教
 | 
			
		||||
              </TabsTrigger>
 | 
			
		||||
            </TabsList>
 | 
			
		||||
            <div className="flex items-center gap-x-2 pr-4 mb-2">
 | 
			
		||||
              <Select value={theme} onValueChange={setTheme}>
 | 
			
		||||
@ -123,6 +152,98 @@ function App() {
 | 
			
		||||
          <ScrollBar orientation="horizontal" />
 | 
			
		||||
        </ScrollArea>
 | 
			
		||||
        <TabsContent value="code-editor" className="h-full mt-0">
 | 
			
		||||
          <div className="flex items-center justify-end gap-x-2 pr-4">
 | 
			
		||||
            <TooltipProvider delayDuration={0}>
 | 
			
		||||
              <Tooltip>
 | 
			
		||||
                <TooltipTrigger asChild>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="outline"
 | 
			
		||||
                    size="icon"
 | 
			
		||||
                    aria-label="Redo"
 | 
			
		||||
                    className="bg-white"
 | 
			
		||||
                    onClick={() => redoChange(editorRef.current!)}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Redo2 size={16} strokeWidth={2} aria-hidden="true" />
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </TooltipTrigger>
 | 
			
		||||
                <TooltipContent className="px-2 py-1 text-xs">
 | 
			
		||||
                  重做
 | 
			
		||||
                </TooltipContent>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            </TooltipProvider>
 | 
			
		||||
            <TooltipProvider delayDuration={0}>
 | 
			
		||||
              <Tooltip>
 | 
			
		||||
                <TooltipTrigger asChild>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="outline"
 | 
			
		||||
                    size="icon"
 | 
			
		||||
                    aria-label="Undo"
 | 
			
		||||
                    className="bg-white"
 | 
			
		||||
                    onClick={() => undoChange(editorRef.current!)}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Undo2 size={16} strokeWidth={2} aria-hidden="true" />
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </TooltipTrigger>
 | 
			
		||||
                <TooltipContent className="px-2 py-1 text-xs">
 | 
			
		||||
                  撤销
 | 
			
		||||
                </TooltipContent>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            </TooltipProvider>
 | 
			
		||||
            <TooltipProvider delayDuration={0}>
 | 
			
		||||
              <Tooltip>
 | 
			
		||||
                <TooltipTrigger asChild>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="outline"
 | 
			
		||||
                    size="icon"
 | 
			
		||||
                    aria-label="Format"
 | 
			
		||||
                    className="bg-white"
 | 
			
		||||
                    onClick={() => formatCode(editorRef.current!)}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Paintbrush size={16} strokeWidth={2} aria-hidden="true" />
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </TooltipTrigger>
 | 
			
		||||
                <TooltipContent className="px-2 py-1 text-xs">
 | 
			
		||||
                  格式化
 | 
			
		||||
                </TooltipContent>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            </TooltipProvider>
 | 
			
		||||
            <TooltipProvider delayDuration={0}>
 | 
			
		||||
              <Tooltip>
 | 
			
		||||
                <TooltipTrigger asChild>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="outline"
 | 
			
		||||
                    size="icon"
 | 
			
		||||
                    aria-label="Copy"
 | 
			
		||||
                    className="bg-white"
 | 
			
		||||
                    onClick={() => copyCode(editorRef.current!)}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Copy size={16} strokeWidth={2} aria-hidden="true" />
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </TooltipTrigger>
 | 
			
		||||
                <TooltipContent className="px-2 py-1 text-xs">
 | 
			
		||||
                  复制
 | 
			
		||||
                </TooltipContent>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            </TooltipProvider>
 | 
			
		||||
            <TooltipProvider delayDuration={0}>
 | 
			
		||||
              <Tooltip>
 | 
			
		||||
                <TooltipTrigger asChild>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="outline"
 | 
			
		||||
                    size="icon"
 | 
			
		||||
                    aria-label="Download"
 | 
			
		||||
                    className="bg-white"
 | 
			
		||||
                    onClick={() => downloadCode(editorRef.current!)}
 | 
			
		||||
                  >
 | 
			
		||||
                    <FileDown size={16} strokeWidth={2} aria-hidden="true" />
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </TooltipTrigger>
 | 
			
		||||
                <TooltipContent className="px-2 py-1 text-xs">
 | 
			
		||||
                  下载
 | 
			
		||||
                </TooltipContent>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            </TooltipProvider>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Editor
 | 
			
		||||
            theme={theme}
 | 
			
		||||
            path={file.path}
 | 
			
		||||
@ -145,6 +266,7 @@ function App() {
 | 
			
		||||
              shikiToMonaco(await highlighter, monaco);
 | 
			
		||||
            }}
 | 
			
		||||
            onMount={(editor: monaco.editor.IStandaloneCodeEditor) => {
 | 
			
		||||
              editorRef.current = editor;
 | 
			
		||||
              const value = editor.getModel()?.getValue();
 | 
			
		||||
              if (value !== undefined) {
 | 
			
		||||
                window.parent.postMessage(
 | 
			
		||||
@ -168,29 +290,6 @@ function App() {
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </TabsContent>
 | 
			
		||||
        <TabsContent value="diff-editor" className="h-full mt-0">
 | 
			
		||||
          <DiffEditor
 | 
			
		||||
            theme={theme}
 | 
			
		||||
            language={file.language}
 | 
			
		||||
            original={file.value}
 | 
			
		||||
            modified={file.value}
 | 
			
		||||
            options={{
 | 
			
		||||
              minimap: { enabled: false },
 | 
			
		||||
              fontSize: 16,
 | 
			
		||||
              showFoldingControls: "always",
 | 
			
		||||
              automaticLayout: true,
 | 
			
		||||
              autoIndent: "full",
 | 
			
		||||
              guides: {
 | 
			
		||||
                bracketPairs: true,
 | 
			
		||||
                indentation: true,
 | 
			
		||||
              },
 | 
			
		||||
              padding: { top: 8 },
 | 
			
		||||
            }}
 | 
			
		||||
            beforeMount={async (monaco: Monaco) => {
 | 
			
		||||
              shikiToMonaco(await highlighter, monaco);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </TabsContent>
 | 
			
		||||
      </Tabs>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										57
									
								
								code-editor/src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								code-editor/src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import { Slot } from "@radix-ui/react-slot"
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const buttonVariants = cva(
 | 
			
		||||
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default:
 | 
			
		||||
          "bg-primary text-primary-foreground shadow hover:bg-primary/90",
 | 
			
		||||
        destructive:
 | 
			
		||||
          "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
 | 
			
		||||
        outline:
 | 
			
		||||
          "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
 | 
			
		||||
        secondary:
 | 
			
		||||
          "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
 | 
			
		||||
        ghost: "hover:bg-accent hover:text-accent-foreground",
 | 
			
		||||
        link: "text-primary underline-offset-4 hover:underline",
 | 
			
		||||
      },
 | 
			
		||||
      size: {
 | 
			
		||||
        default: "h-9 px-4 py-2",
 | 
			
		||||
        sm: "h-8 rounded-md px-3 text-xs",
 | 
			
		||||
        lg: "h-10 rounded-md px-8",
 | 
			
		||||
        icon: "h-9 w-9",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
      size: "default",
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export interface ButtonProps
 | 
			
		||||
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
 | 
			
		||||
    VariantProps<typeof buttonVariants> {
 | 
			
		||||
  asChild?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
 | 
			
		||||
  ({ className, variant, size, asChild = false, ...props }, ref) => {
 | 
			
		||||
    const Comp = asChild ? Slot : "button"
 | 
			
		||||
    return (
 | 
			
		||||
      <Comp
 | 
			
		||||
        className={cn(buttonVariants({ variant, size, className }))}
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        {...props}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
Button.displayName = "Button"
 | 
			
		||||
 | 
			
		||||
export { Button, buttonVariants }
 | 
			
		||||
							
								
								
									
										30
									
								
								code-editor/src/components/ui/tooltip.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								code-editor/src/components/ui/tooltip.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const TooltipProvider = TooltipPrimitive.Provider
 | 
			
		||||
 | 
			
		||||
const Tooltip = TooltipPrimitive.Root
 | 
			
		||||
 | 
			
		||||
const TooltipTrigger = TooltipPrimitive.Trigger
 | 
			
		||||
 | 
			
		||||
const TooltipContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof TooltipPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
 | 
			
		||||
>(({ className, sideOffset = 4, ...props }, ref) => (
 | 
			
		||||
  <TooltipPrimitive.Portal>
 | 
			
		||||
    <TooltipPrimitive.Content
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      sideOffset={sideOffset}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  </TooltipPrimitive.Portal>
 | 
			
		||||
))
 | 
			
		||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
 | 
			
		||||
 | 
			
		||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
 | 
			
		||||
							
								
								
									
										46
									
								
								code-editor/src/lib/editor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								code-editor/src/lib/editor.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
			
		||||
import * as monaco from "monaco-editor";
 | 
			
		||||
 | 
			
		||||
export function redoChange(editor: monaco.editor.IStandaloneCodeEditor) {
 | 
			
		||||
  editor.trigger("redo", "redo", null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function undoChange(editor: monaco.editor.IStandaloneCodeEditor) {
 | 
			
		||||
  editor.trigger("undo", "undo", null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatCode(editor: monaco.editor.IStandaloneCodeEditor) {
 | 
			
		||||
  editor.trigger("format", "editor.action.formatDocument", null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function downloadCode(editor: monaco.editor.IStandaloneCodeEditor) {
 | 
			
		||||
  const model = editor.getModel();
 | 
			
		||||
  if (model) {
 | 
			
		||||
    const code = model.getValue();
 | 
			
		||||
    const blob = new Blob([code], { type: "text/plain" });
 | 
			
		||||
    const url = URL.createObjectURL(blob);
 | 
			
		||||
    const a = document.createElement("a");
 | 
			
		||||
    a.href = url;
 | 
			
		||||
    const fileName = model.uri.path.split("/").pop()?.replace(/_/g, "");
 | 
			
		||||
    a.download = fileName || "code.txt";
 | 
			
		||||
    document.body.appendChild(a);
 | 
			
		||||
    a.click();
 | 
			
		||||
    document.body.removeChild(a);
 | 
			
		||||
    URL.revokeObjectURL(url);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function copyCode(editor: monaco.editor.IStandaloneCodeEditor) {
 | 
			
		||||
  const model = editor.getModel();
 | 
			
		||||
  if (model) {
 | 
			
		||||
    navigator.clipboard.writeText(model.getValue());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function moveCursorToEnd(editor: monaco.editor.IStandaloneCodeEditor) {
 | 
			
		||||
  const model = editor.getModel();
 | 
			
		||||
  if (model) {
 | 
			
		||||
    const position = model.getPositionAt(model.getValueLength());
 | 
			
		||||
    editor.setPosition(position);
 | 
			
		||||
    editor.focus();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user