Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
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.

AiReviewCard.tsxBlame232 lines · 1 contributor
9989d86Claude1import React, { useState, useCallback } from 'react';
2import {
3 FlatList,
4 StyleSheet,
5 Text,
6 TouchableOpacity,
7 View,
8} from 'react-native';
9import { colors } from '../theme/colors';
10import type { AiReviewSummary, PrComment } from '../api/pulls';
11
12interface AiReviewCardProps {
13 review: AiReviewSummary;
14}
15
16function 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
24function 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
32interface InlineCommentProps {
33 comment: PrComment;
34}
35
36function 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
59export 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
114const 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});