diff --git a/CLAUDE.md b/CLAUDE.md index 5adb853..b594a11 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -282,9 +282,63 @@ pnpm tsc --noEmit --- -## 8. 文档沉淀规则 +## 8. 文档管理规则 -凡是出现以下情况,应更新 `docs/openfang-knowledge-base.md` 或相关文档: +### 8.1 文档结构 + +```text +docs/ +├── features/ # 功能全景文档 +│ ├── README.md # 功能索引和优先级矩阵 +│ ├── brainstorming-notes.md # 头脑风暴记录 +│ ├── 00-architecture/ # 架构层功能 +│ ├── 01-core-features/ # 核心功能 +│ ├── 02-intelligence-layer/ # 智能层 (L4 自演化) +│ ├── 03-context-database/ # 上下文数据库 +│ ├── 04-skills-ecosystem/ # Skills 生态 +│ ├── 05-hands-system/ # Hands 系统 +│ └── 06-tauri-backend/ # Tauri 后端 +├── knowledge-base/ # 技术知识库 +│ ├── openfang-technical-reference.md +│ ├── openfang-websocket-protocol.md +│ └── troubleshooting.md +└── WORK_SUMMARY_*.md # 工作日志 +``` + +### 8.2 功能文档维护规范 + +**何时更新功能文档**: + +| 触发条件 | 更新内容 | +|---------|---------| +| 新增功能 | 创建新文档,填写设计初衷 | +| 功能修改 | 更新技术设计、预期作用 | +| 功能完成 | 更新实际效果、测试覆盖 | +| 发现问题 | 更新已知问题、风险挑战 | +| 用户反馈 | 更新用户反馈、演化路线 | + +**功能文档模板**: + +```markdown +# [功能名称] + +> **分类**: [架构层/核心功能/智能层/上下文数据库/Skills/Hands/Tauri] +> **优先级**: [P0-决定性 / P1-重要 / P2-增强] +> **成熟度**: [L0-概念 / L1-原型 / L2-可用 / L3-成熟 / L4-生产] +> **最后更新**: YYYY-MM-DD + +## 一、功能概述 +## 二、设计初衷(问题背景、设计目标、竞品参考、设计约束) +## 三、技术设计(核心接口、数据流、状态管理) +## 四、预期作用(用户价值、系统价值、成功指标) +## 五、实际效果(已实现、测试覆盖、已知问题、用户反馈) +## 六、演化路线(短期/中期/长期) +## 七、头脑风暴笔记(待讨论问题、创意想法、风险挑战) +``` + +### 8.3 知识库更新规则 + +凡是出现以下情况,应更新 `docs/knowledge-base/` 或相关文档: - 新的协议坑 (REST/WebSocket) - 新的握手/配置/模型排障结论 @@ -294,6 +348,16 @@ pnpm tsc --noEmit 原则:**修完就记,避免二次踩坑。** +### 8.4 文档质量检查清单 + +每次更新文档后,检查: + +- [ ] 文件路径引用正确 +- [ ] 技术术语统一 +- [ ] ICE 评分已更新 +- [ ] 成熟度等级已更新 +- [ ] 已知问题列表已更新 + --- ## 9. 常见高风险点 @@ -407,3 +471,15 @@ docs(knowledge-base): capture OpenFang RBAC permission issues - [ ] 插件从 TypeScript 改为 SKILL.md - [ ] 添加 Hands/Workflow 相关 UI - [ ] 处理 16 层安全防护的交互 + +--- + +## 16. 参考文档更新 + +- `docs/features/README.md` - 功能索引和优先级矩阵 +- `docs/features/brainstorming-notes.md` - 头脑风暴记录 +- `docs/knowledge-base/openfang-technical-reference.md` - OpenFang 技术参考 +- `docs/knowledge-base/openfang-websocket-protocol.md` - WebSocket 协议 +- `docs/knowledge-base/troubleshooting.md` - 排障指南 +- `skills/` - SKILL.md 技能定义 +- `hands/` - HAND.toml 自主能力包 diff --git a/desktop/src/App.tsx b/desktop/src/App.tsx index 193faf1..8a5c6dc 100644 --- a/desktop/src/App.tsx +++ b/desktop/src/App.tsx @@ -8,6 +8,7 @@ import { SettingsLayout } from './components/Settings/SettingsLayout'; import { HandTaskPanel } from './components/HandTaskPanel'; import { SchedulerPanel } from './components/SchedulerPanel'; import { TeamCollaborationView } from './components/TeamCollaborationView'; +import { SwarmDashboard } from './components/SwarmDashboard'; import { useGatewayStore } from './store/gatewayStore'; import { useTeamStore } from './store/teamStore'; import { getStoredGatewayToken } from './lib/gateway-client'; @@ -110,6 +111,15 @@ function App() { description="Choose a team from the list on the left, or click + to create a new multi-Agent collaboration team." /> ) + ) : mainContentView === 'swarm' ? ( + + + ) : ( )} diff --git a/desktop/src/components/Feedback/FeedbackButton.tsx b/desktop/src/components/Feedback/FeedbackButton.tsx new file mode 100644 index 0000000..687868e --- /dev/null +++ b/desktop/src/components/Feedback/FeedbackButton.tsx @@ -0,0 +1,40 @@ +import { MessageCircle } from 'lucide-react'; +import { motion } from 'framer-motion'; +import { useFeedbackStore } from './feedbackStore'; +import { Button } from '../ui'; + +interface FeedbackButtonProps { + onClick: () => void; + showCount?: boolean; +} + +export function FeedbackButton({ onClick, showCount = true }: FeedbackButtonProps) { + const feedbackItems = useFeedbackStore((state) => state.feedbackItems); + const pendingCount = feedbackItems.filter((f) => f.status === 'pending' || f.status === 'submitted').length; + + return ( + + + + ); +} diff --git a/desktop/src/components/Feedback/FeedbackHistory.tsx b/desktop/src/components/Feedback/FeedbackHistory.tsx new file mode 100644 index 0000000..e1a03fd --- /dev/null +++ b/desktop/src/components/Feedback/FeedbackHistory.tsx @@ -0,0 +1,193 @@ +import { format } from 'date-fns'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Clock, CheckCircle, AlertCircle, Hourglass, Trash2, ChevronDown, ChevronUp } from 'lucide-react'; +import { useFeedbackStore, type FeedbackSubmission, type FeedbackStatus } from './feedbackStore'; +import { Button, Badge } from '../ui'; + +const statusConfig: Record = { + pending: { label: 'Pending', color: 'text-gray-500', icon: }, + submitted: { label: 'Submitted', color: 'text-blue-500', icon: }, + acknowledged: { label: 'Acknowledged', color: 'text-purple-500', icon: }, + in_progress: { label: 'In Progress', color: 'text-yellow-500', icon: }, + resolved: { label: 'Resolved', color: 'text-green-500', icon: }, +}; + +const typeLabels: Record = { + bug: 'Bug Report', + feature: 'Feature Request'; + general: 'General Feedback', +}; +const priorityLabels: Record = { + low: 'Low', + medium: 'Medium', + high: 'High', +}; + +interface FeedbackHistoryProps { + onViewDetails?: (feedback: FeedbackSubmission) => void; +} + +export function FeedbackHistory({ onViewDetails }: FeedbackHistoryProps) { + const { feedbackItems, deleteFeedback, updateFeedbackStatus } = useFeedbackStore(); + const [expandedId, setExpandedId] = useState(null); + + const formatDate = (timestamp: number) => { + return format(new Date(timestamp), 'yyyy-MM-dd HH:mm'); + }; + + const handleDelete = (id: string) => { + if (confirm('Are you sure you want to delete this feedback?')) { + deleteFeedback(id); + } + }; + + const handleStatusChange = (id: string, newStatus: FeedbackStatus) => { + updateFeedbackStatus(id, newStatus); + }; + + if (feedbackItems.length === 0) { + return ( +
+

No feedback submissions yet.

+

Click the feedback button to submit your first feedback.

+
+ ); + } + + return ( +
+ {feedbackItems.map((feedback) => { + const isExpanded = expandedId === feedback.id; + const statusInfo = statusConfig[feedback.status]; + + return ( + + {/* Header */} +
setExpandedId(isExpanded ? null : feedback.id)} + > +
+
+ {feedback.type === 'bug' && } + {feedback.type === 'feature' && } + {feedback.type === 'general' && } +
+
+

+ {feedback.title} +

+

+ {typeLabels[feedback.type]} - {formatDate(feedback.createdAt)} +

+
+ + {priorityLabels[feedback.priority]} + +
+
+ +
+
+ + {/* Expandable Content */} + + {isExpanded && ( + +
+ {/* Description */} +
+
Description
+

+ {feedback.description} +

+
+ + {/* Attachments */} + {feedback.attachments.length > 0 && ( +
+
+ Attachments ({feedback.attachments.length}) +
+
+ {feedback.attachments.map((att, idx) => ( + + {att.name} + + ))} +
+
+ )} + + {/* Metadata */} +
+
System Info
+
+

App Version: {feedback.metadata.appVersion}

+

OS: {feedback.metadata.os}

+

Submitted: {format(feedback.createdAt)}

+
+
+ + {/* Status and Actions */} +
+
+ + {statusInfo.icon} + {statusInfo.label} + +
+
+ + +
+
+
+
+ )} +
+
+ ); + })} +
+ ); +} diff --git a/desktop/src/components/Feedback/FeedbackModal.tsx b/desktop/src/components/Feedback/FeedbackModal.tsx new file mode 100644 index 0000000..7f68838 --- /dev/null +++ b/desktop/src/components/Feedback/FeedbackModal.tsx @@ -0,0 +1,292 @@ +import { useState, useRef } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Send, Bug, Lightbulb, MessageSquare, AlertCircle, Upload, Trash2 } from 'lucide-react'; +import { useFeedbackStore, type FeedbackType, type FeedbackPriority } from './feedbackStore'; +import { Button } from '../ui'; +import { useToast } from '../ui/Toast'; + +interface FeedbackModalProps { + onClose: () => void; +} + +const typeOptions: { value: FeedbackType; label: string; icon: React.ReactNode }[] = [ + { value: 'bug', label: 'Bug Report', icon: }, + { value: 'feature', label: 'Feature Request', icon: }, + { value: 'general', label: 'General Feedback', icon: }, +]; + +const priorityOptions: { value: FeedbackPriority; label: string; color: string }[] = [ + { value: 'low', label: 'Low', color: 'text-gray-500' }, + { value: 'medium', label: 'Medium', color: 'text-yellow-600' }, + { value: 'high', label: 'High', color: 'text-red-500' }, +]; + +export function FeedbackModal({ onClose }: FeedbackModalProps) { + const { submitFeedback, isLoading, error } = useFeedbackStore(); + const { toast } = useToast(); + const fileInputRef = useRef(null); + + const [type, setType] = useState('bug'); + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [priority, setPriority] = useState('medium'); + const [attachments, setAttachments] = useState([]); + + const handleSubmit = async () => { + if (!title.trim() || !description.trim()) { + toast('Please fill in title and description', 'warning'); + return; + } + + // Convert files to base64 for storage + const processedAttachments = await Promise.all( + attachments.map(async (file) => { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => { + resolve({ + name: file.name, + type: file.type, + size: file.size, + data: reader.result as string, + }); + }; + reader.readAsDataURL(file); + }); + }) + ); + + try { + const result = await submitFeedback({ + type, + title: title.trim(), + description: description.trim(), + priority, + attachments: processedAttachments, + metadata: { + appVersion: '0.0.0', + os: navigator.platform, + timestamp: Date.now(), + }, + }); + + if (result) { + toast('Feedback submitted successfully!', 'success'); + // Reset form + setTitle(''); + setDescription(''); + setAttachments([]); + setType('bug'); + setPriority('medium'); + onClose(); + } + } catch (err) { + toast('Failed to submit feedback. Please try again.', 'error'); + } + }; + + const handleFileSelect = (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + // Limit to 5 attachments + const newFiles = [...attachments, ...files].slice(0, 5); + setAttachments(newFiles); + }; + + const removeAttachment = (index: number) => { + setAttachments(attachments.filter((_, i) => i !== index)); + }; + + const formatFileSize = (bytes: number): string => { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + }; + + return ( + + { + if (e.target === e.currentTarget) onClose(); + }} + > + + {/* Header */} +
+

+ Submit Feedback +

+ +
+ + {/* Content */} +
+ {/* Type Selection */} +
+ +
+ {typeOptions.map((opt) => ( + + ))} +
+
+ + {/* Title */} +
+ + setTitle(e.target.value)} + placeholder="Brief summary of your feedback" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-orange-400 dark:bg-gray-700 dark:text-gray-100" + maxLength={100} + /> +
+ + {/* Description */} +
+ +