mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
fix(frontend): show empty state placeholders for quality rules and learnings in view mode
- Modify conditional rendering logic to always display section headers in view mode - Add empty state placeholders when quality_rules/learnings arrays are empty - Add localization keys for empty state messages in English and Chinese - Improves UX by making these features visible even when no entries exist Related files: - ccw/frontend/src/pages/ProjectOverviewPage.tsx - ccw/frontend/src/locales/en/project-overview.json - ccw/frontend/src/locales/zh/project-overview.json
This commit is contained in:
@@ -45,7 +45,54 @@
|
|||||||
"constraints": "Constraints",
|
"constraints": "Constraints",
|
||||||
"qualityRules": "Quality Rules",
|
"qualityRules": "Quality Rules",
|
||||||
"learnings": "Session Learnings",
|
"learnings": "Session Learnings",
|
||||||
"scope": "Scope"
|
"scope": "Scope",
|
||||||
|
"noQualityRules": "No quality rules defined yet. Switch to edit mode to add rules.",
|
||||||
|
"noLearnings": "No learning summaries yet. Switch to edit mode to add learnings.",
|
||||||
|
"edit": "Edit",
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"saveSuccess": "Guidelines saved successfully",
|
||||||
|
"saveError": "Failed to save guidelines",
|
||||||
|
"conventionCategories": {
|
||||||
|
"codingStyle": "Coding Style",
|
||||||
|
"namingPatterns": "Naming Patterns",
|
||||||
|
"fileStructure": "File Structure",
|
||||||
|
"documentation": "Documentation"
|
||||||
|
},
|
||||||
|
"constraintCategories": {
|
||||||
|
"architecture": "Architecture",
|
||||||
|
"techStack": "Tech Stack",
|
||||||
|
"performance": "Performance",
|
||||||
|
"security": "Security"
|
||||||
|
},
|
||||||
|
"qualityRuleFields": {
|
||||||
|
"rule": "Rule",
|
||||||
|
"scope": "Scope",
|
||||||
|
"enforcedBy": "Enforced By",
|
||||||
|
"addRule": "Add Quality Rule",
|
||||||
|
"removeRule": "Remove"
|
||||||
|
},
|
||||||
|
"learningFields": {
|
||||||
|
"insight": "Insight",
|
||||||
|
"category": "Category",
|
||||||
|
"sessionId": "Session ID",
|
||||||
|
"context": "Context",
|
||||||
|
"date": "Date",
|
||||||
|
"addLearning": "Add Learning",
|
||||||
|
"removeLearning": "Remove"
|
||||||
|
},
|
||||||
|
"placeholders": {
|
||||||
|
"addItem": "Press Enter to add item",
|
||||||
|
"rule": "Enter quality rule",
|
||||||
|
"scope": "Enter scope",
|
||||||
|
"enforcedBy": "Enter enforcement method",
|
||||||
|
"insight": "Enter insight",
|
||||||
|
"category": "Enter category",
|
||||||
|
"sessionId": "Enter session ID",
|
||||||
|
"context": "Enter context",
|
||||||
|
"selectDate": "Select date"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"title": "Statistics",
|
"title": "Statistics",
|
||||||
|
|||||||
@@ -45,7 +45,54 @@
|
|||||||
"constraints": "约束",
|
"constraints": "约束",
|
||||||
"qualityRules": "质量规则",
|
"qualityRules": "质量规则",
|
||||||
"learnings": "学习总结",
|
"learnings": "学习总结",
|
||||||
"scope": "范围"
|
"scope": "范围",
|
||||||
|
"noQualityRules": "暂无质量规则。切换到编辑模式添加规则。",
|
||||||
|
"noLearnings": "暂无学习总结。切换到编辑模式添加总结。",
|
||||||
|
"edit": "编辑",
|
||||||
|
"save": "保存",
|
||||||
|
"cancel": "取消",
|
||||||
|
"saving": "正在保存...",
|
||||||
|
"saveSuccess": "规范已保存成功",
|
||||||
|
"saveError": "保存规范失败",
|
||||||
|
"conventionCategories": {
|
||||||
|
"codingStyle": "编码风格",
|
||||||
|
"namingPatterns": "命名规范",
|
||||||
|
"fileStructure": "文件结构",
|
||||||
|
"documentation": "文档规范"
|
||||||
|
},
|
||||||
|
"constraintCategories": {
|
||||||
|
"architecture": "架构",
|
||||||
|
"techStack": "技术栈",
|
||||||
|
"performance": "性能",
|
||||||
|
"security": "安全性"
|
||||||
|
},
|
||||||
|
"qualityRuleFields": {
|
||||||
|
"rule": "规则",
|
||||||
|
"scope": "范围",
|
||||||
|
"enforcedBy": "执行方式",
|
||||||
|
"addRule": "添加质量规则",
|
||||||
|
"removeRule": "删除"
|
||||||
|
},
|
||||||
|
"learningFields": {
|
||||||
|
"insight": "见解",
|
||||||
|
"category": "分类",
|
||||||
|
"sessionId": "会话ID",
|
||||||
|
"context": "上下文",
|
||||||
|
"date": "日期",
|
||||||
|
"addLearning": "添加学习总结",
|
||||||
|
"removeLearning": "删除"
|
||||||
|
},
|
||||||
|
"placeholders": {
|
||||||
|
"addItem": "按Enter添加条目",
|
||||||
|
"rule": "输入质量规则",
|
||||||
|
"scope": "输入范围",
|
||||||
|
"enforcedBy": "输入执行方式",
|
||||||
|
"insight": "输入见解",
|
||||||
|
"category": "输入分类",
|
||||||
|
"sessionId": "输入会话ID",
|
||||||
|
"context": "输入上下文",
|
||||||
|
"selectDate": "选择日期"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"title": "统计",
|
"title": "统计",
|
||||||
|
|||||||
@@ -24,13 +24,20 @@ import {
|
|||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
GitCommitHorizontal,
|
GitCommitHorizontal,
|
||||||
|
Edit,
|
||||||
|
Save,
|
||||||
|
X,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useProjectOverview } from '@/hooks/useProjectOverview';
|
import { useProjectOverview, useUpdateGuidelines } from '@/hooks/useProjectOverview';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import type {
|
import type {
|
||||||
KeyComponent,
|
KeyComponent,
|
||||||
DevelopmentIndexEntry,
|
DevelopmentIndexEntry,
|
||||||
GuidelineEntry,
|
GuidelineEntry,
|
||||||
LearningEntry,
|
LearningEntry,
|
||||||
|
ProjectGuidelines,
|
||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
@@ -61,7 +68,10 @@ function formatDate(dateString: string | undefined): string {
|
|||||||
export function ProjectOverviewPage() {
|
export function ProjectOverviewPage() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { projectOverview, isLoading, error, refetch } = useProjectOverview();
|
const { projectOverview, isLoading, error, refetch } = useProjectOverview();
|
||||||
|
const { updateGuidelines, isUpdating } = useUpdateGuidelines();
|
||||||
const [devIndexView, setDevIndexView] = React.useState<DevIndexView>('category');
|
const [devIndexView, setDevIndexView] = React.useState<DevIndexView>('category');
|
||||||
|
const [isEditMode, setIsEditMode] = React.useState(false);
|
||||||
|
const [editedGuidelines, setEditedGuidelines] = React.useState<ProjectGuidelines | null>(null);
|
||||||
|
|
||||||
// Helper function to format date
|
// Helper function to format date
|
||||||
function formatDate(dateString: string | undefined): string {
|
function formatDate(dateString: string | undefined): string {
|
||||||
@@ -131,6 +141,45 @@ export function ProjectOverviewPage() {
|
|||||||
// Calculate statistics
|
// Calculate statistics
|
||||||
const totalFeatures = devIndexCategories.reduce((sum, cat) => sum + devIndexTotals[cat.key], 0);
|
const totalFeatures = devIndexCategories.reduce((sum, cat) => sum + devIndexTotals[cat.key], 0);
|
||||||
|
|
||||||
|
// Guidelines edit handlers
|
||||||
|
const handleEditStart = React.useCallback(() => {
|
||||||
|
const g = projectOverview?.guidelines;
|
||||||
|
setEditedGuidelines({
|
||||||
|
conventions: {
|
||||||
|
coding_style: [...(g?.conventions?.coding_style || [])],
|
||||||
|
naming_patterns: [...(g?.conventions?.naming_patterns || [])],
|
||||||
|
file_structure: [...(g?.conventions?.file_structure || [])],
|
||||||
|
documentation: [...(g?.conventions?.documentation || [])],
|
||||||
|
},
|
||||||
|
constraints: {
|
||||||
|
architecture: [...(g?.constraints?.architecture || [])],
|
||||||
|
tech_stack: [...(g?.constraints?.tech_stack || [])],
|
||||||
|
performance: [...(g?.constraints?.performance || [])],
|
||||||
|
security: [...(g?.constraints?.security || [])],
|
||||||
|
},
|
||||||
|
quality_rules: (g?.quality_rules || []).map((r) => ({ ...r })),
|
||||||
|
learnings: (g?.learnings || []).map((l) => ({ ...l })),
|
||||||
|
});
|
||||||
|
setIsEditMode(true);
|
||||||
|
}, [projectOverview?.guidelines]);
|
||||||
|
|
||||||
|
const handleEditCancel = React.useCallback(() => {
|
||||||
|
setIsEditMode(false);
|
||||||
|
setEditedGuidelines(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSave = React.useCallback(async () => {
|
||||||
|
if (!editedGuidelines) return;
|
||||||
|
try {
|
||||||
|
await updateGuidelines(editedGuidelines);
|
||||||
|
toast.success(formatMessage({ id: 'projectOverview.guidelines.saveSuccess' }));
|
||||||
|
setIsEditMode(false);
|
||||||
|
setEditedGuidelines(null);
|
||||||
|
} catch {
|
||||||
|
toast.error(formatMessage({ id: 'projectOverview.guidelines.saveError' }));
|
||||||
|
}
|
||||||
|
}, [editedGuidelines, updateGuidelines, formatMessage]);
|
||||||
|
|
||||||
// Render loading state
|
// Render loading state
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -598,139 +647,443 @@ export function ProjectOverviewPage() {
|
|||||||
{guidelines && (
|
{guidelines && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<h3 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<ScrollText className="w-5 h-5" />
|
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||||
{formatMessage({ id: 'projectOverview.guidelines.title' })}
|
<ScrollText className="w-5 h-5" />
|
||||||
</h3>
|
{formatMessage({ id: 'projectOverview.guidelines.title' })}
|
||||||
|
</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{!isEditMode ? (
|
||||||
|
<Button variant="outline" size="sm" onClick={handleEditStart}>
|
||||||
|
<Edit className="w-4 h-4 mr-1" />
|
||||||
|
{formatMessage({ id: 'projectOverview.guidelines.edit' })}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleEditCancel} disabled={isUpdating}>
|
||||||
|
<X className="w-4 h-4 mr-1" />
|
||||||
|
{formatMessage({ id: 'projectOverview.guidelines.cancel' })}
|
||||||
|
</Button>
|
||||||
|
<Button variant="default" size="sm" onClick={handleSave} disabled={isUpdating}>
|
||||||
|
<Save className="w-4 h-4 mr-1" />
|
||||||
|
{isUpdating ? formatMessage({ id: 'projectOverview.guidelines.saving' }) : formatMessage({ id: 'projectOverview.guidelines.save' })}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Conventions */}
|
{!isEditMode ? (
|
||||||
{guidelines.conventions && (
|
<>
|
||||||
<div>
|
{/* Read-only Mode - Conventions */}
|
||||||
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
{guidelines.conventions && (
|
||||||
<BookMarked className="w-4 h-4" />
|
<div>
|
||||||
<span>{formatMessage({ id: 'projectOverview.guidelines.conventions' })}</span>
|
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||||
</h4>
|
<BookMarked className="w-4 h-4" />
|
||||||
<div className="space-y-2">
|
<span>{formatMessage({ id: 'projectOverview.guidelines.conventions' })}</span>
|
||||||
{Object.entries(guidelines.conventions).slice(0, 4).map(([key, items]) => {
|
</h4>
|
||||||
const itemList = Array.isArray(items) ? items : [];
|
<div className="space-y-2">
|
||||||
if (itemList.length === 0) return null;
|
{Object.entries(guidelines.conventions).map(([key, items]) => {
|
||||||
return (
|
const itemList = Array.isArray(items) ? items : [];
|
||||||
<div key={key} className="space-y-1">
|
if (itemList.length === 0) return null;
|
||||||
{itemList.slice(0, 3).map((item: string, i: number) => (
|
return (
|
||||||
<div
|
<div key={key} className="space-y-1">
|
||||||
key={i}
|
{itemList.map((item: string, i: number) => (
|
||||||
className="flex items-start gap-3 p-3 bg-background border border-border rounded-lg"
|
<div
|
||||||
>
|
key={i}
|
||||||
<span className="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">
|
className="flex items-start gap-3 p-3 bg-background border border-border rounded-lg"
|
||||||
{key}
|
>
|
||||||
</span>
|
<span className="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">
|
||||||
<span className="text-sm text-foreground">{item as string}</span>
|
{key}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-foreground">{item}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
</div>
|
})}
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Constraints */}
|
|
||||||
{guidelines.constraints && (
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
|
||||||
<ShieldAlert className="w-4 h-4" />
|
|
||||||
<span>{formatMessage({ id: 'projectOverview.guidelines.constraints' })}</span>
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{Object.entries(guidelines.constraints).slice(0, 4).map(([key, items]) => {
|
|
||||||
const itemList = Array.isArray(items) ? items : [];
|
|
||||||
if (itemList.length === 0) return null;
|
|
||||||
return (
|
|
||||||
<div key={key} className="space-y-1">
|
|
||||||
{itemList.slice(0, 3).map((item: string, i: number) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="flex items-start gap-3 p-3 bg-background border border-border rounded-lg"
|
|
||||||
>
|
|
||||||
<span className="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">
|
|
||||||
{key}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-foreground">{item as string}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Quality Rules */}
|
|
||||||
{guidelines.quality_rules && guidelines.quality_rules.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
|
||||||
<CheckSquare className="w-4 h-4" />
|
|
||||||
<span>{formatMessage({ id: 'projectOverview.guidelines.qualityRules' })}</span>
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{guidelines.quality_rules.slice(0, 5).map((rule: GuidelineEntry, i: number) => (
|
|
||||||
<div key={i} className="p-3 bg-background border border-border rounded-lg">
|
|
||||||
<div className="flex items-start justify-between mb-1">
|
|
||||||
<span className="text-sm text-foreground font-medium">{rule.rule}</span>
|
|
||||||
{rule.enforced_by && (
|
|
||||||
<span className="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">
|
|
||||||
{rule.enforced_by}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{formatMessage({ id: 'projectOverview.guidelines.scope' })}: {rule.scope}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Learnings */}
|
{/* Read-only Mode - Constraints */}
|
||||||
{guidelines.learnings && guidelines.learnings.length > 0 && (
|
{guidelines.constraints && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||||
<Lightbulb className="w-4 h-4" />
|
<ShieldAlert className="w-4 h-4" />
|
||||||
<span>{formatMessage({ id: 'projectOverview.guidelines.learnings' })}</span>
|
<span>{formatMessage({ id: 'projectOverview.guidelines.constraints' })}</span>
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{guidelines.learnings.slice(0, 5).map((learning: LearningEntry, i: number) => (
|
{Object.entries(guidelines.constraints).map(([key, items]) => {
|
||||||
<div
|
const itemList = Array.isArray(items) ? items : [];
|
||||||
key={i}
|
if (itemList.length === 0) return null;
|
||||||
className="p-3 bg-background border border-border rounded-lg border-l-4 border-l-primary"
|
return (
|
||||||
|
<div key={key} className="space-y-1">
|
||||||
|
{itemList.map((item: string, i: number) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex items-start gap-3 p-3 bg-background border border-border rounded-lg"
|
||||||
|
>
|
||||||
|
<span className="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">
|
||||||
|
{key}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-foreground">{item}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Read-only Mode - Quality Rules */}
|
||||||
|
{guidelines.quality_rules && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||||
|
<CheckSquare className="w-4 h-4" />
|
||||||
|
<span>{formatMessage({ id: 'projectOverview.guidelines.qualityRules' })}</span>
|
||||||
|
</h4>
|
||||||
|
{guidelines.quality_rules.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{guidelines.quality_rules.map((rule: GuidelineEntry, i: number) => (
|
||||||
|
<div key={i} className="p-3 bg-background border border-border rounded-lg">
|
||||||
|
<div className="flex items-start justify-between mb-1">
|
||||||
|
<span className="text-sm text-foreground font-medium">{rule.rule}</span>
|
||||||
|
{rule.enforced_by && (
|
||||||
|
<span className="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">
|
||||||
|
{rule.enforced_by}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{formatMessage({ id: 'projectOverview.guidelines.scope' })}: {rule.scope}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground italic">
|
||||||
|
{formatMessage({ id: 'projectOverview.guidelines.noQualityRules' }) || 'No quality rules defined yet. Switch to edit mode to add rules.'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Read-only Mode - Learnings */}
|
||||||
|
{guidelines.learnings && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||||
|
<Lightbulb className="w-4 h-4" />
|
||||||
|
<span>{formatMessage({ id: 'projectOverview.guidelines.learnings' })}</span>
|
||||||
|
</h4>
|
||||||
|
{guidelines.learnings.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{guidelines.learnings.map((learning: LearningEntry, i: number) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="p-3 bg-background border border-border rounded-lg border-l-4 border-l-primary"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<span className="text-sm text-foreground">{learning.insight}</span>
|
||||||
|
<span className="text-xs text-muted-foreground whitespace-nowrap ml-2">
|
||||||
|
{formatDate(learning.date)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
{learning.category && (
|
||||||
|
<span className="px-2 py-0.5 bg-muted text-muted-foreground rounded">
|
||||||
|
{learning.category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{learning.session_id && (
|
||||||
|
<span className="px-2 py-0.5 bg-primary-light text-primary rounded font-mono">
|
||||||
|
{learning.session_id}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{learning.context && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">{learning.context}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground italic">
|
||||||
|
{formatMessage({ id: 'projectOverview.guidelines.noLearnings' }) || 'No learning summaries yet. Switch to edit mode to add learnings.'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Edit Mode - Conventions */}
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||||
|
<BookMarked className="w-4 h-4" />
|
||||||
|
<span>{formatMessage({ id: 'projectOverview.guidelines.conventions' })}</span>
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Object.entries({
|
||||||
|
coding_style: 'codingStyle',
|
||||||
|
naming_patterns: 'namingPatterns',
|
||||||
|
file_structure: 'fileStructure',
|
||||||
|
documentation: 'documentation',
|
||||||
|
}).map(([key, labelKey]) => (
|
||||||
|
<div key={key}>
|
||||||
|
<label className="text-xs font-medium text-muted-foreground mb-1 block">
|
||||||
|
{formatMessage({ id: `projectOverview.guidelines.conventionCategories.${labelKey}` })}
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-2 p-2 border border-border rounded-lg bg-background min-h-[40px]">
|
||||||
|
{((editedGuidelines?.conventions as any)?.[key] || []).map((item: string, idx: number) => (
|
||||||
|
<div key={idx} className="flex items-center gap-1 px-2 py-1 bg-muted rounded text-sm">
|
||||||
|
<span>{item}</span>
|
||||||
|
<button
|
||||||
|
className="ml-1 text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
(updated.conventions as any)[key] = ((updated.conventions as any)[key] as string[]).filter((_: string, i: number) => i !== idx);
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="flex-1 min-w-[120px] bg-transparent outline-none text-sm"
|
||||||
|
placeholder={formatMessage({ id: 'projectOverview.guidelines.placeholders.addItem' })}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && e.currentTarget.value.trim()) {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
(updated.conventions as any)[key] = [...((updated.conventions as any)[key] || []), e.currentTarget.value.trim()];
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
e.currentTarget.value = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Edit Mode - Constraints */}
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||||
|
<ShieldAlert className="w-4 h-4" />
|
||||||
|
<span>{formatMessage({ id: 'projectOverview.guidelines.constraints' })}</span>
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Object.entries({
|
||||||
|
architecture: 'architecture',
|
||||||
|
tech_stack: 'techStack',
|
||||||
|
performance: 'performance',
|
||||||
|
security: 'security',
|
||||||
|
}).map(([key, labelKey]) => (
|
||||||
|
<div key={key}>
|
||||||
|
<label className="text-xs font-medium text-muted-foreground mb-1 block">
|
||||||
|
{formatMessage({ id: `projectOverview.guidelines.constraintCategories.${labelKey}` })}
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-2 p-2 border border-border rounded-lg bg-background min-h-[40px]">
|
||||||
|
{((editedGuidelines?.constraints as any)?.[key] || []).map((item: string, idx: number) => (
|
||||||
|
<div key={idx} className="flex items-center gap-1 px-2 py-1 bg-muted rounded text-sm">
|
||||||
|
<span>{item}</span>
|
||||||
|
<button
|
||||||
|
className="ml-1 text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
(updated.constraints as any)[key] = ((updated.constraints as any)[key] as string[]).filter((_: string, i: number) => i !== idx);
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="flex-1 min-w-[120px] bg-transparent outline-none text-sm"
|
||||||
|
placeholder={formatMessage({ id: 'projectOverview.guidelines.placeholders.addItem' })}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && e.currentTarget.value.trim()) {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
(updated.constraints as any)[key] = [...((updated.constraints as any)[key] || []), e.currentTarget.value.trim()];
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
e.currentTarget.value = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Edit Mode - Quality Rules */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h4 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
||||||
|
<CheckSquare className="w-4 h-4" />
|
||||||
|
<span>{formatMessage({ id: 'projectOverview.guidelines.qualityRules' })}</span>
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
updated.quality_rules = [...(updated.quality_rules || []), { rule: '', scope: '', enforced_by: '' }];
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-2">
|
<Plus className="w-4 h-4 mr-1" />
|
||||||
<span className="text-sm text-foreground">{learning.insight}</span>
|
{formatMessage({ id: 'projectOverview.guidelines.qualityRuleFields.addRule' })}
|
||||||
<span className="text-xs text-muted-foreground whitespace-nowrap ml-2">
|
</Button>
|
||||||
{formatDate(learning.date)}
|
</div>
|
||||||
</span>
|
<div className="space-y-2">
|
||||||
|
{(editedGuidelines?.quality_rules || []).map((rule, idx) => (
|
||||||
|
<div key={idx} className="p-3 border border-border rounded-lg space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="flex-1 px-2 py-1 text-sm border border-border rounded bg-background"
|
||||||
|
placeholder={formatMessage({ id: 'projectOverview.guidelines.placeholders.rule' })}
|
||||||
|
value={rule.rule}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
updated.quality_rules![idx].rule = e.target.value;
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="text-destructive hover:bg-destructive/10 p-2 rounded"
|
||||||
|
onClick={() => {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
updated.quality_rules = updated.quality_rules!.filter((_, i) => i !== idx);
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="px-2 py-1 text-sm border border-border rounded bg-background"
|
||||||
|
placeholder={formatMessage({ id: 'projectOverview.guidelines.placeholders.scope' })}
|
||||||
|
value={rule.scope}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
updated.quality_rules![idx].scope = e.target.value;
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="px-2 py-1 text-sm border border-border rounded bg-background"
|
||||||
|
placeholder={formatMessage({ id: 'projectOverview.guidelines.placeholders.enforcedBy' })}
|
||||||
|
value={rule.enforced_by || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
updated.quality_rules![idx].enforced_by = e.target.value;
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
))}
|
||||||
{learning.category && (
|
</div>
|
||||||
<span className="px-2 py-0.5 bg-muted text-muted-foreground rounded">
|
|
||||||
{learning.category}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{learning.session_id && (
|
|
||||||
<span className="px-2 py-0.5 bg-primary-light text-primary rounded font-mono">
|
|
||||||
{learning.session_id}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{learning.context && (
|
|
||||||
<p className="text-xs text-muted-foreground mt-2">{learning.context}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Edit Mode - Learnings */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h4 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
||||||
|
<Lightbulb className="w-4 h-4" />
|
||||||
|
<span>{formatMessage({ id: 'projectOverview.guidelines.learnings' })}</span>
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
updated.learnings = [...(updated.learnings || []), { insight: '', category: '', session_id: '', context: '', date: new Date().toISOString() }];
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-1" />
|
||||||
|
{formatMessage({ id: 'projectOverview.guidelines.learningFields.addLearning' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(editedGuidelines?.learnings || []).map((learning, idx) => (
|
||||||
|
<div key={idx} className="p-3 border border-border rounded-lg space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="flex-1 px-2 py-1 text-sm border border-border rounded bg-background"
|
||||||
|
placeholder={formatMessage({ id: 'projectOverview.guidelines.placeholders.insight' })}
|
||||||
|
value={learning.insight}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
updated.learnings![idx].insight = e.target.value;
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="text-destructive hover:bg-destructive/10 p-2 rounded"
|
||||||
|
onClick={() => {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
updated.learnings = updated.learnings!.filter((_, i) => i !== idx);
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="px-2 py-1 text-sm border border-border rounded bg-background"
|
||||||
|
placeholder={formatMessage({ id: 'projectOverview.guidelines.placeholders.category' })}
|
||||||
|
value={learning.category || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
updated.learnings![idx].category = e.target.value;
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="px-2 py-1 text-sm border border-border rounded bg-background"
|
||||||
|
placeholder={formatMessage({ id: 'projectOverview.guidelines.placeholders.sessionId' })}
|
||||||
|
value={learning.session_id || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
updated.learnings![idx].session_id = e.target.value;
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
className="w-full px-2 py-1 text-sm border border-border rounded bg-background"
|
||||||
|
placeholder={formatMessage({ id: 'projectOverview.guidelines.placeholders.context' })}
|
||||||
|
rows={2}
|
||||||
|
value={learning.context || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = { ...editedGuidelines };
|
||||||
|
updated.learnings![idx].context = e.target.value;
|
||||||
|
setEditedGuidelines(updated);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user