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 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 04:09:00 +08:00
parent f43a640e64
commit e850613972
12 changed files with 375 additions and 213 deletions

View File

@@ -1,4 +1,9 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import Editor from 'react-simple-code-editor';
import { highlight, languages } from 'prismjs';
import 'prismjs/components/prism-markdown';
import 'prismjs/components/prism-yaml';
import 'prismjs/themes/prism.css';
import { useAccounts } from '@/hooks/useAccounts';
import { usePrompts } from '@/hooks/usePrompts';
import { Button } from '@/components/ui/button';
@@ -9,6 +14,13 @@ const PROMPT_TYPES = [
{ type: 'video', label: '视频提示词' },
] as const;
const editorStyle = {
fontFamily: '"ui-monospace", "SFMono-Regular", "SF Mono", Menlo, Consolas, monospace',
fontSize: 13,
lineHeight: 1.6,
minHeight: '100%',
};
export function PromptEditor() {
const { accounts } = useAccounts();
const [selectedAccount, setSelectedAccount] = useState<string>('');
@@ -27,15 +39,23 @@ export function PromptEditor() {
}
}, [selectedAccount, selectedType]);
const highlightCode = useCallback((code: string) => {
try {
return highlight(code, languages.markdown, 'markdown');
} catch {
return code;
}
}, []);
return (
<div className="flex h-full">
<div className="w-48 border-r border-zinc-800 p-3 space-y-3">
<div className="w-48 border-r border-zinc-200 p-3 space-y-3 bg-zinc-50">
<div>
<label className="text-xs text-zinc-500"></label>
<label className="text-xs font-medium text-zinc-500"></label>
<select
value={selectedAccount}
onChange={(e) => setSelectedAccount(e.target.value)}
className="mt-1 w-full h-9 rounded-md border border-zinc-200 bg-zinc-50 px-2 text-sm"
className="mt-1 w-full h-9 rounded-md border border-zinc-200 bg-white px-2 text-sm text-zinc-700"
>
{accounts.map((a) => (
<option key={a.id} value={a.id}>{a.name}</option>
@@ -43,14 +63,14 @@ export function PromptEditor() {
</select>
</div>
<div>
<label className="text-xs text-zinc-500"></label>
<label className="text-xs font-medium text-zinc-500"></label>
<div className="mt-1 space-y-0.5">
{PROMPT_TYPES.map(({ type, label }) => (
<button
key={type}
onClick={() => setSelectedType(type)}
className={`w-full text-left px-2 py-1.5 rounded text-sm ${
selectedType === type ? 'bg-indigo-50 text-indigo-700 font-medium' : 'text-zinc-500 hover:text-zinc-700 hover:bg-zinc-50'
className={`w-full text-left px-2 py-1.5 rounded text-sm transition-colors ${
selectedType === type ? 'bg-indigo-50 text-indigo-700 font-medium' : 'text-zinc-500 hover:text-zinc-700 hover:bg-white'
}`}
>
{label}
@@ -60,24 +80,29 @@ export function PromptEditor() {
</div>
</div>
<div className="flex-1 flex flex-col">
<div className="flex items-center justify-between px-4 py-2 border-b border-zinc-800">
<span className="text-xs text-zinc-500 font-mono">{path}</span>
<div className="flex-1 flex flex-col bg-white">
<div className="flex items-center justify-between px-4 py-2 border-b border-zinc-200">
<span className="text-xs text-zinc-400 font-mono">{path || '选择账户和模板'}</span>
<Button
size="sm"
onClick={() => save(selectedAccount, selectedType, content)}
disabled={!selectedAccount || loading}
className="text-xs"
>
{loading ? '加载中...' : '保存'}
</Button>
</div>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
className="flex-1 w-full bg-white text-zinc-700 font-mono text-sm p-4 resize-none outline-none"
placeholder="加载中..."
spellCheck={false}
/>
<div className="flex-1 overflow-auto">
<Editor
value={content}
onValueChange={setContent}
highlight={highlightCode}
padding={16}
style={editorStyle}
className="min-h-full bg-white"
textareaClassName="outline-none"
/>
</div>
</div>
</div>
);