From e850613972f31b9aa3499b2dc8eac8e175d55559 Mon Sep 17 00:00:00 2001 From: sion123 <450702724@qq.com> Date: Thu, 7 May 2026 04:09:00 +0800 Subject: [PATCH] feat(web): add markdown syntax highlighting, slash commands, stop button, quote/reply - PromptEditor with Prism.js syntax highlighting - Slash commands (/run, /status, /images, /list, /help) in chat input - Stop button to cancel ongoing generation - Quote/reply and regenerate/continue actions in chat - MiddlePanel with conversation timestamps and preview - Pipeline progress in chat view - Fix all remaining dark theme classes Co-Authored-By: Claude Opus 4.7 --- .claude/skills/config.json | 4 +- web/client/package-lock.json | 29 ++++ web/client/package.json | 3 + web/client/src/components/chat/ChatInput.tsx | 91 +++++++++--- .../src/components/chat/ChatMessage.tsx | 84 ++++++++--- web/client/src/components/chat/ChatView.tsx | 133 +++++++++++------- .../src/components/prompts/PromptEditor.tsx | 61 +++++--- web/client/src/hooks/useChat.ts | 55 +++++--- web/client/src/lib/websocket.ts | 18 +-- web/data/meitu-agent.db-shm | Bin 32768 -> 32768 bytes web/data/meitu-agent.db-wal | Bin 3596792 -> 3675072 bytes web/server/ws/chat.ts | 110 +++++---------- 12 files changed, 375 insertions(+), 213 deletions(-) diff --git a/.claude/skills/config.json b/.claude/skills/config.json index 6e59330..ad2bb4b 100644 --- a/.claude/skills/config.json +++ b/.claude/skills/config.json @@ -1,6 +1,6 @@ { -"jianyingDraftPath": "/Users/lc/Movies/JianyingPro/User Data/Projects/com.lveditor.draft", - "capcutMateDir": "/Users/lc/capcut-mate", + "jianyingDraftPath": "C:/Users/45070/AppData/Local/JianyingPro/User Data/Projects/com.lveditor.draft", + "capcutMateDir": "C:/Users/45070/capcut-mate", "capcutMateApiBase": "http://capcut.muyetools.cn/openapi/capcut-mate/v1", "imgbbApiKey": "deprecated", "geminiApiBaseUrl": "https://yunwu.ai", diff --git a/web/client/package-lock.json b/web/client/package-lock.json index 461bc97..772cf23 100644 --- a/web/client/package-lock.json +++ b/web/client/package-lock.json @@ -12,14 +12,17 @@ "clsx": "^2.1.0", "lucide-react": "^0.460.0", "markdown-it": "^14.1.0", + "prismjs": "^1.30.0", "react": "^18.3.0", "react-dom": "^18.3.0", "react-router-dom": "^7.15.0", + "react-simple-code-editor": "^0.14.1", "tailwind-merge": "^2.6.0", "zustand": "^5.0.0" }, "devDependencies": { "@types/markdown-it": "^14.1.0", + "@types/prismjs": "^1.26.6", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "@types/react-router-dom": "^5.3.3", @@ -1246,6 +1249,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -2336,6 +2346,15 @@ "dev": true, "license": "MIT" }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -2439,6 +2458,16 @@ "react-dom": ">=18" } }, + "node_modules/react-simple-code-editor": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.14.1.tgz", + "integrity": "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/web/client/package.json b/web/client/package.json index f4a9109..ad5ad08 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -12,14 +12,17 @@ "clsx": "^2.1.0", "lucide-react": "^0.460.0", "markdown-it": "^14.1.0", + "prismjs": "^1.30.0", "react": "^18.3.0", "react-dom": "^18.3.0", "react-router-dom": "^7.15.0", + "react-simple-code-editor": "^0.14.1", "tailwind-merge": "^2.6.0", "zustand": "^5.0.0" }, "devDependencies": { "@types/markdown-it": "^14.1.0", + "@types/prismjs": "^1.26.6", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "@types/react-router-dom": "^5.3.3", diff --git a/web/client/src/components/chat/ChatInput.tsx b/web/client/src/components/chat/ChatInput.tsx index 4056b3a..8455b84 100644 --- a/web/client/src/components/chat/ChatInput.tsx +++ b/web/client/src/components/chat/ChatInput.tsx @@ -1,11 +1,34 @@ -import { useState, useRef } from 'react'; -import { Send } from 'lucide-react'; +import { useState, useRef, useEffect } from 'react'; +import { Send, Terminal, Image, Play, FileText } from 'lucide-react'; import { Button } from '@/components/ui/button'; +const SLASH_COMMANDS = [ + { cmd: '/run', desc: '执行 pipeline 阶段', icon: Play }, + { cmd: '/status', desc: '查看管线进度', icon: Terminal }, + { cmd: '/images', desc: '生成图片', icon: Image }, + { cmd: '/list', desc: '列出可用账号', icon: FileText }, + { cmd: '/help', desc: '显示帮助', icon: Terminal }, +]; + export function ChatInput({ onSend, disabled }: { onSend: (content: string) => void; disabled?: boolean }) { const [input, setInput] = useState(''); + const [showCmds, setShowCmds] = useState(false); + const [cmdIdx, setCmdIdx] = useState(0); const ref = useRef(null); + useEffect(() => { + if (input.startsWith('/')) { + setShowCmds(true); + setCmdIdx(0); + } else { + setShowCmds(false); + } + }, [input]); + + const matchingCmds = input.startsWith('/') + ? SLASH_COMMANDS.filter((c) => c.cmd.startsWith(input.split(' ')[0])) + : []; + const handleSend = () => { if (!input.trim()) return; onSend(input.trim()); @@ -14,6 +37,18 @@ export function ChatInput({ onSend, disabled }: { onSend: (content: string) => v }; const handleKeyDown = (e: React.KeyboardEvent) => { + if (showCmds && matchingCmds.length > 0) { + if (e.key === 'Tab' || e.key === 'Enter') { + e.preventDefault(); + const cmd = matchingCmds[cmdIdx % matchingCmds.length].cmd; + setInput(cmd + ' '); + setShowCmds(false); + return; + } + if (e.key === 'ArrowDown') { e.preventDefault(); setCmdIdx((i) => (i + 1) % matchingCmds.length); return; } + if (e.key === 'ArrowUp') { e.preventDefault(); setCmdIdx((i) => (i - 1 + matchingCmds.length) % matchingCmds.length); return; } + } + if (e.key === 'Escape') { setShowCmds(false); return; } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); @@ -21,21 +56,43 @@ export function ChatInput({ onSend, disabled }: { onSend: (content: string) => v }; return ( -
-
-