From 0eab45c49cdebda46216d31057e0ee08c1b62a8e Mon Sep 17 00:00:00 2001 From: weishu Date: Sun, 28 Dec 2025 13:10:46 +0800 Subject: [PATCH] refactor: consolidate copy-to-clipboard logic and icons --- web/src/components/CodeBlock.tsx | 70 ++----------------- .../components/assistant-ui/markdown-text.tsx | 69 ++---------------- web/src/components/icons.tsx | 19 +++++ web/src/hooks/useCopyToClipboard.ts | 23 ++++++ web/src/lib/clipboard.ts | 6 ++ web/src/routes/sessions/file.tsx | 67 ++---------------- 6 files changed, 63 insertions(+), 191 deletions(-) create mode 100644 web/src/hooks/useCopyToClipboard.ts create mode 100644 web/src/lib/clipboard.ts diff --git a/web/src/components/CodeBlock.tsx b/web/src/components/CodeBlock.tsx index d3c34fee..f4ea8838 100644 --- a/web/src/components/CodeBlock.tsx +++ b/web/src/components/CodeBlock.tsx @@ -1,86 +1,26 @@ -import { useState } from 'react' -import { usePlatform } from '@/hooks/usePlatform' +import { useCopyToClipboard } from '@/hooks/useCopyToClipboard' import { useShikiHighlighter } from '@/lib/shiki' - -function safeCopyToClipboard(text: string): Promise { - if (navigator.clipboard?.writeText) { - return navigator.clipboard.writeText(text) - } - return Promise.reject(new Error('Clipboard API not available')) -} - -function CopyIcon(props: { className?: string }) { - return ( - - - - - ) -} - -function CheckIcon(props: { className?: string }) { - return ( - - - - ) -} +import { CopyIcon, CheckIcon } from '@/components/icons' export function CodeBlock(props: { code: string language?: string showCopyButton?: boolean }) { - const { haptic } = usePlatform() const showCopyButton = props.showCopyButton ?? true - - const [copied, setCopied] = useState(false) - + const { copied, copy } = useCopyToClipboard() const highlighted = useShikiHighlighter(props.code, props.language) - const handleCopy = async () => { - try { - await safeCopyToClipboard(props.code) - haptic.notification('success') - setCopied(true) - setTimeout(() => setCopied(false), 1500) - } catch { - haptic.notification('error') - } - } - return (
{showCopyButton ? ( ) : null} diff --git a/web/src/components/assistant-ui/markdown-text.tsx b/web/src/components/assistant-ui/markdown-text.tsx index 171a151a..326c5fb9 100644 --- a/web/src/components/assistant-ui/markdown-text.tsx +++ b/web/src/components/assistant-ui/markdown-text.tsx @@ -1,5 +1,4 @@ import type { ComponentPropsWithoutRef } from 'react' -import { useState } from 'react' import { MarkdownTextPrimitive, unstable_memoizeMarkdownComponents as memoizeMarkdownComponents, @@ -7,75 +6,17 @@ import { type CodeHeaderProps, } from '@assistant-ui/react-markdown' import remarkGfm from 'remark-gfm' -import { getPlatform } from '@/hooks/usePlatform' import { cn } from '@/lib/utils' import { SyntaxHighlighter } from '@/components/assistant-ui/shiki-highlighter' +import { useCopyToClipboard } from '@/hooks/useCopyToClipboard' +import { CopyIcon, CheckIcon } from '@/components/icons' export const MARKDOWN_PLUGINS = [remarkGfm] -function safeCopyToClipboard(text: string): Promise { - if (navigator.clipboard?.writeText) { - return navigator.clipboard.writeText(text) - } - return Promise.reject(new Error('Clipboard API not available')) -} - -function CopyIcon(props: { className?: string }) { - return ( - - - - - ) -} - -function CheckIcon(props: { className?: string }) { - return ( - - - - ) -} - function CodeHeader(props: CodeHeaderProps) { - const [copied, setCopied] = useState(false) - + const { copied, copy } = useCopyToClipboard() const language = props.language && props.language !== 'unknown' ? props.language : '' - const handleCopy = async () => { - const { haptic } = getPlatform() - try { - await safeCopyToClipboard(props.code) - haptic.notification('success') - setCopied(true) - setTimeout(() => setCopied(false), 1500) - } catch { - haptic.notification('error') - } - } - return (
@@ -83,11 +24,11 @@ function CodeHeader(props: CodeHeaderProps) {
) diff --git a/web/src/components/icons.tsx b/web/src/components/icons.tsx index 041ae55f..5200e05f 100644 --- a/web/src/components/icons.tsx +++ b/web/src/components/icons.tsx @@ -41,3 +41,22 @@ export function PlusCircleIcon(props: IconProps) { props ) } + +export function CopyIcon(props: IconProps) { + return createIcon( + <> + + + , + props, + 2 + ) +} + +export function CheckIcon(props: IconProps) { + return createIcon( + , + props, + 2 + ) +} diff --git a/web/src/hooks/useCopyToClipboard.ts b/web/src/hooks/useCopyToClipboard.ts new file mode 100644 index 00000000..dee2666f --- /dev/null +++ b/web/src/hooks/useCopyToClipboard.ts @@ -0,0 +1,23 @@ +import { useState, useCallback } from 'react' +import { usePlatform } from './usePlatform' +import { safeCopyToClipboard } from '@/lib/clipboard' + +export function useCopyToClipboard(resetDelay = 1500) { + const [copied, setCopied] = useState(false) + const { haptic } = usePlatform() + + const copy = useCallback(async (text: string) => { + try { + await safeCopyToClipboard(text) + haptic.notification('success') + setCopied(true) + setTimeout(() => setCopied(false), resetDelay) + return true + } catch { + haptic.notification('error') + return false + } + }, [haptic, resetDelay]) + + return { copied, copy } +} diff --git a/web/src/lib/clipboard.ts b/web/src/lib/clipboard.ts new file mode 100644 index 00000000..79dc74b7 --- /dev/null +++ b/web/src/lib/clipboard.ts @@ -0,0 +1,6 @@ +export function safeCopyToClipboard(text: string): Promise { + if (navigator.clipboard?.writeText) { + return navigator.clipboard.writeText(text) + } + return Promise.reject(new Error('Clipboard API not available')) +} diff --git a/web/src/routes/sessions/file.tsx b/web/src/routes/sessions/file.tsx index 054f0fd5..afb710e3 100644 --- a/web/src/routes/sessions/file.tsx +++ b/web/src/routes/sessions/file.tsx @@ -3,9 +3,10 @@ import { useQuery } from '@tanstack/react-query' import { useParams, useSearch } from '@tanstack/react-router' import type { GitCommandResponse } from '@/types/api' import { FileIcon } from '@/components/FileIcon' +import { CopyIcon, CheckIcon } from '@/components/icons' import { useAppContext } from '@/lib/app-context' import { useAppGoBack } from '@/hooks/useAppGoBack' -import { usePlatform } from '@/hooks/usePlatform' +import { useCopyToClipboard } from '@/hooks/useCopyToClipboard' import { queryKeys } from '@/lib/query-keys' import { langAlias, useShikiHighlighter } from '@/lib/shiki' @@ -120,55 +121,9 @@ function extractCommandError(result: GitCommandResponse | undefined): string | n return result.error ?? result.stderr ?? 'Failed to load diff' } -function safeCopyToClipboard(text: string): Promise { - if (navigator.clipboard?.writeText) { - return navigator.clipboard.writeText(text) - } - return Promise.reject(new Error('Clipboard API not available')) -} - -function CopyIcon(props: { className?: string }) { - return ( - - - - - ) -} - -function CheckIcon(props: { className?: string }) { - return ( - - - - ) -} - export default function FilePage() { const { api } = useAppContext() - const { haptic } = usePlatform() + const { copied, copy } = useCopyToClipboard() const goBack = useAppGoBack() const { sessionId } = useParams({ from: '/sessions/$sessionId/file' }) const search = useSearch({ from: '/sessions/$sessionId/file' }) @@ -218,18 +173,6 @@ export default function FilePage() { const highlighted = useShikiHighlighter(decodedContent, language) const [displayMode, setDisplayMode] = useState<'diff' | 'file'>('diff') - const [copied, setCopied] = useState(false) - - const handleCopyPath = async () => { - try { - await safeCopyToClipboard(filePath) - haptic.notification('success') - setCopied(true) - setTimeout(() => setCopied(false), 1500) - } catch { - haptic.notification('error') - } - } useEffect(() => { if (diffSuccess && !diffContent) { @@ -272,11 +215,11 @@ export default function FilePage() { {filePath}