Blame · Line-by-line history
AiReviewCard.tsx
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 9989d86 | 1 | import React, { useState, useCallback } from 'react'; |
| 2 | import { | |
| 3 | FlatList, | |
| 4 | StyleSheet, | |
| 5 | Text, | |
| 6 | TouchableOpacity, | |
| 7 | View, | |
| 8 | } from 'react-native'; | |
| 9 | import { colors } from '../theme/colors'; | |
| 10 | import type { AiReviewSummary, PrComment } from '../api/pulls'; | |
| 11 | ||
| 12 | interface AiReviewCardProps { | |
| 13 | review: AiReviewSummary; | |
| 14 | } | |
| 15 | ||
| 16 | function severityColor(severity: AiReviewSummary['severity']): string { | |
| 17 | switch (severity) { | |
| 18 | case 'error': return colors.accentRed; | |
| 19 | case 'warning': return colors.accentYellow; | |
| 20 | case 'info': return colors.accentBlue; | |
| 21 | } | |
| 22 | } | |
| 23 | ||
| 24 | function severityLabel(severity: AiReviewSummary['severity']): string { | |
| 25 | switch (severity) { | |
| 26 | case 'error': return 'Needs Changes'; | |
| 27 | case 'warning': return 'Suggestions'; | |
| 28 | case 'info': return 'Looks Good'; | |
| 29 | } | |
| 30 | } | |
| 31 | ||
| 32 | interface InlineCommentProps { | |
| 33 | comment: PrComment; | |
| 34 | } | |
| 35 | ||
| 36 | function InlineComment({ comment }: InlineCommentProps): React.ReactElement { | |
| 37 | return ( | |
| 38 | <View style={styles.inlineComment}> | |
| 39 | {comment.filePath !== null && ( | |
| 40 | <View style={styles.fileHeader}> | |
| 41 | <Text style={styles.filePath} numberOfLines={1}> | |
| 42 | {comment.filePath} | |
| 43 | {comment.lineNumber !== null ? `:${comment.lineNumber}` : ''} | |
| 44 | </Text> | |
| 45 | </View> | |
| 46 | )} | |
| 47 | {comment.diffHunk !== null && ( | |
| 48 | <View style={styles.diffHunk}> | |
| 49 | <Text style={styles.diffHunkText} numberOfLines={4}> | |
| 50 | {comment.diffHunk} | |
| 51 | </Text> | |
| 52 | </View> | |
| 53 | )} | |
| 54 | <Text style={styles.commentBody}>{comment.body}</Text> | |
| 55 | </View> | |
| 56 | ); | |
| 57 | } | |
| 58 | ||
| 59 | export function AiReviewCard({ review }: AiReviewCardProps): React.ReactElement { | |
| 60 | const [expanded, setExpanded] = useState(true); | |
| 61 | const toggle = useCallback(() => setExpanded((v) => !v), []); | |
| 62 | const borderColor = severityColor(review.severity); | |
| 63 | const inlineComments = review.comments.filter((c) => c.filePath !== null); | |
| 64 | ||
| 65 | const renderComment = useCallback( | |
| 66 | ({ item }: { item: PrComment }) => <InlineComment comment={item} />, | |
| 67 | [], | |
| 68 | ); | |
| 69 | ||
| 70 | const keyExtractor = useCallback((item: PrComment) => String(item.id), []); | |
| 71 | ||
| 72 | return ( | |
| 73 | <View style={[styles.card, { borderColor }]}> | |
| 74 | <TouchableOpacity style={styles.header} onPress={toggle} activeOpacity={0.8}> | |
| 75 | <View style={styles.headerLeft}> | |
| 76 | <Text style={styles.sparkle}>✦</Text> | |
| 77 | <Text style={styles.headerTitle}>AI Review</Text> | |
| 78 | <View style={[styles.severityBadge, { borderColor }]}> | |
| 79 | <Text style={[styles.severityText, { color: borderColor }]}> | |
| 80 | {severityLabel(review.severity)} | |
| 81 | </Text> | |
| 82 | </View> | |
| 83 | {inlineComments.length > 0 && ( | |
| 84 | <View style={styles.countBadge}> | |
| 85 | <Text style={styles.countText}>{inlineComments.length} comments</Text> | |
| 86 | </View> | |
| 87 | )} | |
| 88 | </View> | |
| 89 | <Text style={styles.chevron}>{expanded ? '▾' : '▸'}</Text> | |
| 90 | </TouchableOpacity> | |
| 91 | ||
| 92 | {expanded && ( | |
| 93 | <View style={styles.body}> | |
| 94 | <Text style={styles.summary}>{review.summary}</Text> | |
| 95 | ||
| 96 | {inlineComments.length > 0 && ( | |
| 97 | <View style={styles.inlineSection}> | |
| 98 | <Text style={styles.inlineSectionTitle}>Inline Comments</Text> | |
| 99 | <FlatList | |
| 100 | data={inlineComments} | |
| 101 | keyExtractor={keyExtractor} | |
| 102 | renderItem={renderComment} | |
| 103 | scrollEnabled={false} | |
| 104 | ItemSeparatorComponent={() => <View style={styles.separator} />} | |
| 105 | /> | |
| 106 | </View> | |
| 107 | )} | |
| 108 | </View> | |
| 109 | )} | |
| 110 | </View> | |
| 111 | ); | |
| 112 | } | |
| 113 | ||
| 114 | const styles = StyleSheet.create({ | |
| 115 | card: { | |
| 116 | backgroundColor: colors.bgSecondary, | |
| 117 | borderRadius: 8, | |
| 118 | borderWidth: 1, | |
| 119 | marginHorizontal: 16, | |
| 120 | marginBottom: 16, | |
| 121 | overflow: 'hidden', | |
| 122 | }, | |
| 123 | header: { | |
| 124 | flexDirection: 'row', | |
| 125 | alignItems: 'center', | |
| 126 | justifyContent: 'space-between', | |
| 127 | padding: 14, | |
| 128 | backgroundColor: colors.bgTertiary, | |
| 129 | }, | |
| 130 | headerLeft: { | |
| 131 | flexDirection: 'row', | |
| 132 | alignItems: 'center', | |
| 133 | gap: 8, | |
| 134 | flex: 1, | |
| 135 | }, | |
| 136 | sparkle: { | |
| 137 | fontSize: 15, | |
| 138 | color: colors.accentPurple, | |
| 139 | }, | |
| 140 | headerTitle: { | |
| 141 | fontSize: 14, | |
| 142 | fontWeight: '600', | |
| 143 | color: colors.accentPurple, | |
| 144 | }, | |
| 145 | severityBadge: { | |
| 146 | paddingHorizontal: 8, | |
| 147 | paddingVertical: 2, | |
| 148 | borderRadius: 20, | |
| 149 | borderWidth: 1, | |
| 150 | }, | |
| 151 | severityText: { | |
| 152 | fontSize: 11, | |
| 153 | fontWeight: '600', | |
| 154 | }, | |
| 155 | countBadge: { | |
| 156 | paddingHorizontal: 7, | |
| 157 | paddingVertical: 2, | |
| 158 | borderRadius: 20, | |
| 159 | backgroundColor: colors.bg, | |
| 160 | borderWidth: 1, | |
| 161 | borderColor: colors.border, | |
| 162 | }, | |
| 163 | countText: { | |
| 164 | fontSize: 11, | |
| 165 | color: colors.textMuted, | |
| 166 | }, | |
| 167 | chevron: { | |
| 168 | fontSize: 14, | |
| 169 | color: colors.textMuted, | |
| 170 | marginLeft: 8, | |
| 171 | }, | |
| 172 | body: { | |
| 173 | padding: 14, | |
| 174 | gap: 14, | |
| 175 | }, | |
| 176 | summary: { | |
| 177 | fontSize: 14, | |
| 178 | color: colors.text, | |
| 179 | lineHeight: 22, | |
| 180 | }, | |
| 181 | inlineSection: { | |
| 182 | gap: 8, | |
| 183 | }, | |
| 184 | inlineSectionTitle: { | |
| 185 | fontSize: 12, | |
| 186 | fontWeight: '600', | |
| 187 | color: colors.textMuted, | |
| 188 | textTransform: 'uppercase', | |
| 189 | letterSpacing: 0.5, | |
| 190 | }, | |
| 191 | inlineComment: { | |
| 192 | backgroundColor: colors.bg, | |
| 193 | borderRadius: 6, | |
| 194 | overflow: 'hidden', | |
| 195 | borderWidth: 1, | |
| 196 | borderColor: colors.border, | |
| 197 | }, | |
| 198 | fileHeader: { | |
| 199 | paddingHorizontal: 10, | |
| 200 | paddingVertical: 6, | |
| 201 | backgroundColor: colors.bgTertiary, | |
| 202 | borderBottomWidth: 1, | |
| 203 | borderBottomColor: colors.border, | |
| 204 | }, | |
| 205 | filePath: { | |
| 206 | fontSize: 12, | |
| 207 | fontFamily: 'monospace', | |
| 208 | color: colors.textLink, | |
| 209 | }, | |
| 210 | diffHunk: { | |
| 211 | paddingHorizontal: 10, | |
| 212 | paddingVertical: 6, | |
| 213 | backgroundColor: colors.bg, | |
| 214 | borderBottomWidth: 1, | |
| 215 | borderBottomColor: colors.border, | |
| 216 | }, | |
| 217 | diffHunkText: { | |
| 218 | fontSize: 11, | |
| 219 | fontFamily: 'monospace', | |
| 220 | color: colors.textMuted, | |
| 221 | lineHeight: 16, | |
| 222 | }, | |
| 223 | commentBody: { | |
| 224 | padding: 10, | |
| 225 | fontSize: 13, | |
| 226 | color: colors.text, | |
| 227 | lineHeight: 20, | |
| 228 | }, | |
| 229 | separator: { | |
| 230 | height: 8, | |
| 231 | }, | |
| 232 | }); |