name: 设计组件技能 description: 为共情账本设计的组件技能。用于讲故事卡片、故事卡片、个人资料展示以及任何需要文化敏感性的UI组件。提供数据映射、AI丰富模式和设计系统指南。
设计组件技能
此技能为在共情账本中设计和实现UI组件提供全面的指导,特别关注讲故事卡片、数据展示模式和AI驱动的内容丰富。
设计系统基础
色彩板(CSS变量)
/* 使用语义色彩支持暗模式 */
--background /* 页面背景 */
--foreground /* 主要文本 */
--card /* 卡片表面 */
--card-foreground/* 卡片文本 */
--muted /* 柔和背景 */
--muted-foreground /* 二级文本 */
--popover /* 下拉/弹出背景 */
--border /* 边框 */
--primary /* 主要操作 */
--accent /* 辅助/高亮(阳光黄) */
--destructive /* 错误/警告 */
文化色彩含义
| 颜色 | 含义 | 用途 |
|---|---|---|
| 琥珀/金 | 长者智慧,特色 | 长者徽章,特色指示器 |
| 翡翠 | 成长,社区 | 故事计数,活跃状态 |
| 紫色 | 神圣,知识 | 知识守护者徽章 |
| 陶土 | 地球,联系 | 文化归属 |
| 鼠尾草 | 平静,尊重 | 通用UI元素 |
讲故事者卡片数据模型
核心展示字段
interface StorytellerCardData {
// 身份(始终显示)
id: string
display_name: string
avatar_url?: string
pronouns?: string
// 文化背景(可用时显示)
cultural_background?: string
cultural_affiliations?: string[]
traditional_territory?: string
languages_spoken?: string[]
// 状态指示器
is_elder: boolean
is_featured: boolean
status: 'active' | 'inactive' | 'pending'
traditional_knowledge_keeper?: boolean
// 故事指标
story_count: number
featured_quote?: string
expertise_themes?: string[]
// 专业背景
occupation?: string
years_of_experience?: number
specialties?: string[]
// AI丰富的领域
ai_summary?: string
theme_expertise?: string[]
connection_strength?: number
suggested_connections?: string[]
}
数据优先级层次
第一层 - 始终显示:
├── display_name
├── avatar(或首字母回退)
├── cultural_background
└── story_count
第二层 - 在卡片上显示:
├── 长者状态徽章
├── 特色徽章
├── 前3个专业
├── 主要位置
└── 特色引用(如果可用)
第三层 - 在悬停/展开时显示:
├── 完整简介
├── 所有专业
├── 语言
├── 组织
└── 主题专长
第四层 - 仅在个人资料页面上显示:
├── 联系信息
├── 完整故事列表
├── 连接图
└── 详细分析
卡片变体
默认卡片
<StorytellerCard
storyteller={storyteller}
variant="default"
showStories={true}
showActions={true}
/>
- 最小宽度320px,响应式
- 头像,名称,文化背景
- 故事计数,专业(最多3个)
- 长者/特色徽章
- 悬停效果带箭头
紧凑卡片
<StorytellerCard
storyteller={storyteller}
variant="compact"
/>
- 宽度280px,内联布局
- 头像(更小),仅名称
- 故事计数作为徽章
- 适用于侧边栏,列表
特色卡片
<StorytellerCard
storyteller={storyteller}
variant="featured"
/>
- 全宽,更大的头像
- 特色引用显示
- 主题专长徽章
- 渐变背景
- 增强悬停动画
列表视图行
<StorytellerListCard storyteller={storyteller} />
- 全宽水平
- 更多数据可见
- 右侧操作按钮
- 适用于管理视图
AI丰富机会
1. 生物增强
// API: POST /api/storytellers/{id}/enhance-bio
interface BioEnhancement {
original_bio: string
enhanced_bio: string // 语法,流程改进
key_themes: string[] // 从生物中提取
suggested_specialties: string[]
cultural_keywords: string[]
}
2. 特色引用提取
// 从讲故事者的故事中提取引人注目的引用
interface QuoteExtraction {
quotes: Array<{
text: string
story_id: string
themes: string[]
impact_score: number
}>
suggested_featured: string // 最佳引用用于卡片
}
3. 主题专长分析
// 分析所有故事以确定专长领域
interface ThemeExpertise {
primary_themes: string[] // 最多讨论的前3个
secondary_themes: string[] // 支持主题
unique_perspective: string // 使他们独特
theme_depth_scores: Record<string, number>
}
4. 连接建议
// 查找具有互补主题的讲故事者
interface ConnectionSuggestion {
storyteller_id: string
connection_type: 'theme_overlap' | 'geographic' | 'community'
overlap_score: number
reason: string // "Both share expertise in healing stories"
}
5. 摘要生成
// 生成简洁的讲故事者摘要
interface StoritellerSummary {
one_liner: string // "Elder from Wurundjeri Country..."
card_summary: string // 50-100字符用于卡片
full_summary: string // 200-300字符用于个人资料
voice_style: string // "Warm and reflective storytelling"
}
组件实现模式
头像与状态指示器
<div className="relative">
<Avatar
src={storyteller.avatar_url}
fallback={getInitials(storyteller.display_name)}
className="w-16 h-16 border-2 border-background shadow-md"
/>
{/* 状态徽章 - 定位顶部右侧 */}
<div className="absolute -top-1 -right-1 flex gap-1">
{storyteller.is_featured && (
<Badge variant="featured" size="icon">
<Star className="w-3 h-3" />
</Badge>
)}
{storyteller.is_elder && (
<Badge variant="elder" size="icon">
<Crown className="w-3 h-3" />
</Badge>
)}
</div>
</div>
文化背景展示
{/* 尊重文化展示 */}
<div className="flex items-center gap-2 text-muted-foreground">
<MapPin className="w-4 h-4 text-terracotta-500" />
<span className="text-sm">
{storyteller.cultural_background}
{storyteller.traditional_territory && (
<span className="text-xs ml-1">
({storyteller.traditional_territory})
</span>
)}
</span>
</div>
故事指标展示
<div className="flex items-center gap-4">
<div className="flex items-center gap-1.5">
<BookOpen className="w-4 h-4 text-emerald-500" />
<span className="font-semibold">{storyteller.story_count}</span>
<span className="text-muted-foreground text-sm">
{storyteller.story_count === 1 ? 'Story' : 'Stories'}
</span>
</div>
{storyteller.years_of_experience && (
<div className="flex items-center gap-1.5">
<Calendar className="w-4 h-4 text-blue-500" />
<span className="font-semibold">{storyteller.years_of_experience}</span>
<span className="text-muted-foreground text-sm">Years</span>
</div>
)}
</div>
主题/专业徽章
{/* 在卡片上最多显示3个,其余在悬停/详细 */}
<div className="flex flex-wrap gap-1.5">
{storyteller.specialties?.slice(0, 3).map((specialty, i) => (
<Badge
key={specialty}
variant="secondary"
className={cn(
"text-xs",
specialtyColors[i % specialtyColors.length]
)}
>
{specialty}
</Badge>
))}
{(storyteller.specialties?.length || 0) > 3 && (
<Badge variant="outline" className="text-xs text-muted-foreground">
+{storyteller.specialties!.length - 3} more
</Badge>
)}
</div>
特色引用展示
{storyteller.featured_quote && (
<div className="mt-4 p-3 bg-muted/50 rounded-lg border-l-2 border-accent">
<Quote className="w-4 h-4 text-accent mb-1" />
<p className="text-sm italic text-foreground/80 line-clamp-2">
"{storyteller.featured_quote}"
</p>
</div>
)}
AI丰富API模式
触发丰富
// POST /api/storytellers/{id}/enrich
async function enrichStoryteller(storytellerId: string) {
const response = await fetch(`/api/storytellers/${storytellerId}/enrich`, {
method: 'POST',
body: JSON.stringify({
enrich_bio: true,
extract_quotes: true,
analyze_themes: true,
suggest_connections: true
})
})
// 返回丰富作业ID以轮询
return response.json()
}
显示丰富数据
// 检查AI丰富的领域
const hasAIData = storyteller.ai_summary || storyteller.theme_expertise?.length
{hasAIData && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Sparkles className="w-3 h-3 text-accent" />
<span>AI Enhanced</span>
</div>
)}
数据库领域AI丰富
配置文件表添加
-- AI生成摘要
ALTER TABLE profiles ADD COLUMN ai_summary TEXT;
-- 从故事分析中提取的主题专长(来自故事分析)
ALTER TABLE profiles ADD COLUMN theme_expertise TEXT[];
-- AI生成的特色引用选择
ALTER TABLE profiles ADD COLUMN featured_quote TEXT;
ALTER TABLE profiles ADD COLUMN featured_quote_story_id UUID;
-- 连接强度分数
ALTER TABLE profiles ADD COLUMN connection_scores JSONB DEFAULT '{}';
-- 丰富状态
ALTER TABLE profiles ADD COLUMN ai_enrichment_status TEXT DEFAULT 'pending';
ALTER TABLE profiles ADD COLUMN ai_enriched_at TIMESTAMPTZ;
讲故事者连接视图
CREATE VIEW storyteller_connections AS
SELECT
p1.id as storyteller_id,
p2.id as connected_storyteller_id,
array_length(
ARRAY(SELECT unnest(p1.theme_expertise) INTERSECT SELECT unnest(p2.theme_expertise)),
1
) as theme_overlap,
p1.cultural_background = p2.cultural_background as same_culture
FROM profiles p1
CROSS JOIN profiles p2
WHERE p1.id != p2.id
AND p1.is_storyteller = true
AND p2.is_storyteller = true;
组件清单
创建卡片组件前
- [ ] 确定所有所需数据字段
- [ ] 定义哪些字段是必需的与可选的
- [ ] 计划缺失数据的回退状态
- [ ] 考虑暗模式颜色
- [ ] 添加悬停状态
- [ ] 计划加载骨架
- [ ] 添加辅助标签
文化敏感性
- [ ] 使用尊重的术语
- [ ] 突出显示长者状态
- [ ] 尊重地显示文化背景
- [ ] 承认传统领土
- [ ] 不挪用文化符号
- [ ] 尊重地显示语言
AI丰富
- [ ] 标记AI生成的内容
- [ ] 允许用户覆盖AI建议
- [ ] 优雅地处理缺失的AI数据
- [ ] 显示丰富进行中状态
参考组件
| 组件 | 位置 | 目的 |
|---|---|---|
StorytellerCard |
src/components/storyteller/storyteller-card.tsx |
主要卡片组件 |
UnifiedStorytellerCard |
src/components/storyteller/unified-storyteller-card.tsx |
灵活的卡片变体 |
ElegantStorytellerCard |
src/components/storyteller/elegant-storyteller-card.tsx |
高级设计变体 |
StoryCard |
src/components/story/story-card.tsx |
故事展示卡片 |
QuoteCard |
src/components/ui/quote-card.tsx |
引用展示 |
ThemeBadge |
src/components/ui/theme-badge.tsx |
主题展示 |
联合仪表板设计模式(NEW - Sprint 4)
同意状态徽章
目的: 视觉指示联合同意状态
变体:
type ConsentStatus = 'approved' | 'pending' | 'revoked' | 'expired'
const statusConfig = {
approved: { color: 'sage', icon: CheckCircle, label: 'Active' },
pending: { color: 'amber', icon: Clock, label: 'Pending' },
revoked: { color: 'ember', icon: XCircle, label: 'Revoked' },
expired: { color: 'muted', icon: AlertCircle, label: 'Expired' }
}
设计:
<Badge className={cn(
"flex items-center gap-1.5",
status === 'approved' && "bg-sage-100 text-sage-900",
status === 'pending' && "bg-amber-100 text-amber-900",
status === 'revoked' && "bg-ember-100 text-ember-900",
status === 'expired' && "bg-muted text-muted-foreground"
)}>
<Icon className="h-3 w-3" />
{label}
</Badge>
同意卡片布局
目的: 显示单个联合同意与网站信息和控件
结构:
┌─────────────────────────────────────────────┐
│ [Site Logo] JusticeHub │
│ justicehub.org.au │
│ │
│ 状态:[Active Badge] │
│ 文化水平:Public │
│ 创建:Jan 5, 2026 │
│ │
│ 📊 456 views • Last accessed 2 hours ago │
│ │
│ [View Analytics] [Revoke Access] │
└─────────────────────────────────────────────┘
颜色:
- 卡片边框: 使用网站品牌颜色(如果提供)
- 状态徽章: 遵循上述状态配置
- 文化水平: 使用文化颜色(粘土/鼠尾草/天空)
- 操作: 主要(鼠尾草)用于分析,破坏性(火红)用于撤销
嵌入令牌显示
目的: 显示令牌并注意安全遮罩
模式:
<div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-muted rounded font-mono text-sm">
{tokenMasked ? 'LRK•••••••••••XA' : token}
</code>
<Button variant="ghost" size="sm" onClick={() => setTokenMasked(!tokenMasked)}>
{tokenMasked ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
</Button>
<Button variant="ghost" size="sm" onClick={handleCopy}>
<Copy className="h-4 w-4" />
</Button>
</div>
安全:
- 默认:令牌遮罩
- 点击显示(临时,10秒)
- 复制按钮带有确认提示
- 永远不记录未遮罩的令牌
分析图表样式
目的: 跨仪表板的一致图表设计
颜色(Recharts):
const siteColors = {
justicehub: '#C85A54', // Ember
actfarm: '#6B8E72', // Sage
theharvest: '#D97757', // Clay
actplacemat: '#4A90A4' // Sky
}
// 使用
<Line
type="monotone"
dataKey="justicehub"
stroke={siteColors.justicehub}
strokeWidth={2}
dot={{ fill: siteColors.justicehub, r: 4 }}
activeDot={{ r: 6 }}
/>
网格 & 轴:
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis
dataKey="date"
stroke="hsl(var(--muted-foreground))"
fontSize={12}
/>
<YAxis
stroke="hsl(var(--muted-foreground))"
fontSize={12}
/>
撤销对话框
目的: 用文化信息确认同意撤销
布局:
<Dialog>
<DialogHeader>
<DialogTitle className="text-ember-900">
撤销JusticeHub的同意?
</DialogTitle>
<DialogDescription>
这将立即移除JusticeHub对您故事的访问权限。
他们将不再能够在他们的平台上展示它。
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="rounded-lg bg-sky-50 p-4 border border-sky-200">
<p className="text-sm text-sky-900 font-medium">
✨ 您完全控制
</p>
<p className="text-sm text-sky-700 mt-1">
您的故事仍然在共情账本上。您可以随时再次授予同意。
</p>
</div>
<div className="space-y-2">
<Label htmlFor="reason">撤销原因(可选)</Label>
<Textarea
id="reason"
placeholder="例如,故事需要更新,未准备好外部分享..."
rows={3}
/>
<p className="text-xs text-muted-foreground">
这有助于我们改进平台(与JusticeHub共享匿名同意,如果同意是活跃的)。
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onCancel}>
保持同意
</Button>
<Button variant="destructive" onClick={handleRevoke}>
撤销访问
</Button>
</DialogFooter>
</Dialog>
信息原则:
- ✅ 确认讲故事者控制(“您完全控制”)
- ✅ 清晰解释后果(“立即移除访问权限”)
- ✅ 保证可逆性(“再次授予同意”)
- ❌ 没有内疚语言(“您确定吗?”)
- ❌ 没有恐惧语言(“这不能撤销”)
文化许可级别指示器
目的: 用文化背景显示内容敏感性级别
级别:
const culturalLevels = {
public: {
color: 'sage',
icon: Globe,
label: 'Public',
description: 'Safe to share widely'
},
community: {
color: 'clay',
icon: Users,
label: 'Community',
description: 'Indigenous communities only'
},
restricted: {
color: 'amber',
icon: Lock,
label: 'Restricted',
description: 'Requires elder approval'
},
sacred: {
color: 'ember',
icon: ShieldAlert,
label: 'Sacred',
description: 'Not for external sharing'
}
}
组件:
<div className="flex items-center gap-2">
<Icon className="h-4 w-4 text-{color}-700" />
<div>
<p className="text-sm font-medium">{label}</p>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
</div>
联合空状态
尚未同意:
<div className="text-center py-12 space-y-4">
<div className="mx-auto w-16 h-16 rounded-full bg-sage-100 flex items-center justify-center">
<Share2 className="h-8 w-8 text-sage-700" />
</div>
<div>
<h3 className="text-lg font-semibold">No syndication consents yet</h3>
<p className="text-muted-foreground mt-1">
Your stories are safe with you. When you're ready to share with
external platforms like JusticeHub, you'll see them here.
</p>
</div>
<Button onClick={handleCreateConsent}>
<Plus className="mr-2 h-4 w-4" />
Share a Story
</Button>
</div>
全部同意已撤销:
<div className="text-center py-12 space-y-4">
<div className="mx-auto w-16 h-16 rounded-full bg-sky-100 flex items-center justify-center">
<ShieldCheck className="h-8 w-8 text-sky-700" />
</div>
<div>
<h3 className="text-lg font-semibold">You're in control</h3>
<p className="text-muted-foreground mt-1">
All your stories have been removed from external platforms.
You can re-share whenever you're ready.
</p>
</div>
</div>
何时使用此技能
调用时:
- 设计新卡片组件
- 向讲故事者/故事卡片添加字段
- 实施AI丰富功能
- 创建个人资料展示
- 构建带有讲故事者数据的列表视图
- 向UI添加文化指标
- 实施悬停/展开状态
- 创建加载骨架
- 设计联合仪表板UI
- 构建同意管理界面
- 创建分析可视化
- 实施撤销工作流程