docs(guide): rewrite CLAUDE.md with ZCLAW-first perspective
Major changes: - Shift from "OpenFang desktop client" to "independent AI Agent desktop app" - Add decision principle: "Is this useful for ZCLAW? Does it affect ZCLAW?" - Simplify project structure and tech stack sections - Replace OpenClaw vs OpenFang comparison with unified backend approach - Consolidate troubleshooting from scattered sections into organized FAQ - Update Hands system documentation with 8 capabilities and status - Stream
This commit is contained in:
@@ -2,21 +2,19 @@
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useChatStore, Message } from '../store/chatStore';
|
||||
import { useGatewayStore } from '../store/gatewayStore';
|
||||
import { Paperclip, ChevronDown, Terminal, SquarePen, ArrowUp, MessageSquare } from 'lucide-react';
|
||||
import { Paperclip, ChevronDown, Terminal, SquarePen, ArrowUp, MessageSquare, Download, Copy, Check } from 'lucide-react';
|
||||
import { Button, EmptyState } from './ui';
|
||||
import { listItemVariants, defaultTransition, fadeInVariants } from '../lib/animations';
|
||||
import { FirstConversationPrompt } from './FirstConversationPrompt';
|
||||
import { MessageSearch } from './MessageSearch';
|
||||
|
||||
const MODELS = ['glm-5', 'qwen3.5-plus', 'kimi-k2.5', 'minimax-m2.5'];
|
||||
|
||||
export function ChatArea() {
|
||||
const {
|
||||
messages, currentAgent, isStreaming, currentModel,
|
||||
sendMessage: sendToGateway, setCurrentModel, initStreamListener,
|
||||
newConversation,
|
||||
} = useChatStore();
|
||||
const { connectionState, clones } = useGatewayStore();
|
||||
const { connectionState, clones, models } = useGatewayStore();
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const [showModelPicker, setShowModelPicker] = useState(false);
|
||||
@@ -213,16 +211,22 @@ export function ChatArea() {
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
</Button>
|
||||
{showModelPicker && (
|
||||
<div className="absolute bottom-full right-8 mb-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg py-1 min-w-[160px] z-10">
|
||||
{MODELS.map((model) => (
|
||||
<button
|
||||
key={model}
|
||||
onClick={() => { setCurrentModel(model); setShowModelPicker(false); }}
|
||||
className={`w-full text-left px-3 py-2 text-xs hover:bg-gray-50 dark:hover:bg-gray-700 ${model === currentModel ? 'text-orange-600 dark:text-orange-400 font-medium' : 'text-gray-700 dark:text-gray-300'}`}
|
||||
>
|
||||
{model}
|
||||
</button>
|
||||
))}
|
||||
<div className="absolute bottom-full right-8 mb-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg py-1 min-w-[160px] max-h-48 overflow-y-auto z-10">
|
||||
{models.length > 0 ? (
|
||||
models.map((model) => (
|
||||
<button
|
||||
key={model.id}
|
||||
onClick={() => { setCurrentModel(model.id); setShowModelPicker(false); }}
|
||||
className={`w-full text-left px-3 py-2 text-xs hover:bg-gray-50 dark:hover:bg-gray-700 ${model.id === currentModel ? 'text-orange-600 dark:text-orange-400 font-medium' : 'text-gray-700 dark:text-gray-300'}`}
|
||||
>
|
||||
{model.name}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="px-3 py-2 text-xs text-gray-400">
|
||||
{connected ? '加载中...' : '未连接 Gateway'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
@@ -246,6 +250,105 @@ export function ChatArea() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Code block with copy and download functionality */
|
||||
function CodeBlock({ code, language, index }: { code: string; language: string; index: number }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
// Infer filename from language or content
|
||||
const inferFilename = (): string => {
|
||||
const extMap: Record<string, string> = {
|
||||
javascript: 'js', typescript: 'ts', python: 'py', rust: 'rs',
|
||||
go: 'go', java: 'java', cpp: 'cpp', c: 'c', csharp: 'cs',
|
||||
html: 'html', css: 'css', scss: 'scss', json: 'json',
|
||||
yaml: 'yaml', yml: 'yaml', xml: 'xml', sql: 'sql',
|
||||
shell: 'sh', bash: 'sh', powershell: 'ps1',
|
||||
markdown: 'md', md: 'md', dockerfile: 'dockerfile',
|
||||
};
|
||||
|
||||
// Check if language contains a filename (e.g., ```app.tsx)
|
||||
if (language.includes('.') || language.includes('/')) {
|
||||
return language;
|
||||
}
|
||||
|
||||
// Check for common patterns in code
|
||||
const codeLower = code.toLowerCase();
|
||||
if (codeLower.includes('<!doctype html') || codeLower.includes('<html')) {
|
||||
return 'index.html';
|
||||
}
|
||||
if (codeLower.includes('package.json') || (codeLower.includes('"name"') && codeLower.includes('"version"'))) {
|
||||
return 'package.json';
|
||||
}
|
||||
if (codeLower.startsWith('{') && (codeLower.includes('"import"') || codeLower.includes('"export"'))) {
|
||||
return 'config.json';
|
||||
}
|
||||
|
||||
// Use language extension
|
||||
const ext = extMap[language.toLowerCase()] || language.toLowerCase();
|
||||
return `code-${index + 1}.${ext || 'txt'}`;
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
setDownloading(true);
|
||||
try {
|
||||
const filename = inferFilename();
|
||||
const blob = new Blob([code], { type: 'text/plain;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.error('Failed to download:', err);
|
||||
}
|
||||
setTimeout(() => setDownloading(false), 500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative group my-2">
|
||||
<pre className="bg-gray-900 text-gray-100 rounded-lg p-3 overflow-x-auto text-xs font-mono leading-relaxed">
|
||||
{language && (
|
||||
<div className="text-gray-500 text-[10px] mb-1 uppercase flex items-center justify-between">
|
||||
<span>{language}</span>
|
||||
</div>
|
||||
)}
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
{/* Action buttons - show on hover */}
|
||||
<div className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="p-1.5 bg-gray-700 hover:bg-gray-600 rounded text-gray-300 hover:text-white transition-colors"
|
||||
title="复制代码"
|
||||
>
|
||||
{copied ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="p-1.5 bg-gray-700 hover:bg-gray-600 rounded text-gray-300 hover:text-white transition-colors"
|
||||
title="下载文件"
|
||||
disabled={downloading}
|
||||
>
|
||||
<Download className={`w-3.5 h-3.5 ${downloading ? 'animate-pulse' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Lightweight markdown renderer — handles code blocks, inline code, bold, italic, links */
|
||||
function renderMarkdown(text: string): React.ReactNode[] {
|
||||
const nodes: React.ReactNode[] = [];
|
||||
@@ -266,10 +369,7 @@ function renderMarkdown(text: string): React.ReactNode[] {
|
||||
}
|
||||
i++; // skip closing ```
|
||||
nodes.push(
|
||||
<pre key={nodes.length} className="bg-gray-900 text-gray-100 rounded-lg p-3 my-2 overflow-x-auto text-xs font-mono leading-relaxed">
|
||||
{lang && <div className="text-gray-500 text-[10px] mb-1 uppercase">{lang}</div>}
|
||||
<code>{codeLines.join('\n')}</code>
|
||||
</pre>
|
||||
<CodeBlock key={nodes.length} code={codeLines.join('\n')} language={lang} index={nodes.length} />
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -354,6 +454,22 @@ function MessageBubble({ message }: { message: Message }) {
|
||||
// 思考中状态:streaming 且内容为空时显示思考指示器
|
||||
const isThinking = message.streaming && !message.content;
|
||||
|
||||
// Download message as Markdown file
|
||||
const handleDownloadMessage = () => {
|
||||
if (!message.content) return;
|
||||
const timestamp = new Date().toISOString().slice(0, 10);
|
||||
const filename = `message-${timestamp}.md`;
|
||||
const blob = new Blob([message.content], { type: 'text/markdown;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex gap-4 ${isUser ? 'justify-end' : ''}`}>
|
||||
<div
|
||||
@@ -373,7 +489,7 @@ function MessageBubble({ message }: { message: Message }) {
|
||||
<span className="text-sm">思考中...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`p-4 shadow-sm ${isUser ? 'chat-bubble-user shadow-md' : 'chat-bubble-assistant'}`}>
|
||||
<div className={`p-4 shadow-sm ${isUser ? 'chat-bubble-user shadow-md' : 'chat-bubble-assistant'} relative group`}>
|
||||
<div className={`leading-relaxed whitespace-pre-wrap ${isUser ? 'text-white' : 'text-gray-700 dark:text-gray-200'}`}>
|
||||
{message.content
|
||||
? (isUser ? message.content : renderMarkdown(message.content))
|
||||
@@ -383,6 +499,16 @@ function MessageBubble({ message }: { message: Message }) {
|
||||
{message.error && (
|
||||
<p className="text-xs text-red-500 mt-2">{message.error}</p>
|
||||
)}
|
||||
{/* Download button for AI messages - show on hover */}
|
||||
{!isUser && message.content && !message.streaming && (
|
||||
<button
|
||||
onClick={handleDownloadMessage}
|
||||
className="absolute top-2 right-2 p-1.5 bg-gray-200/80 dark:bg-gray-700/80 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors opacity-0 group-hover:opacity-100"
|
||||
title="下载为 Markdown"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user