swadhinbiswas feat: OpenCodeHub v1.1.0 - Enterprise Features & Storage Adapters · 2/15/2026
719a1cb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Check, Loader2, Copy } from "lucide-react";
import { toast } from "sonner";
interface CommentItemProps {
comment: any; // Type strictly if possible, but any is fine for MVP
owner: string;
repo: string;
pullNumber: number;
currentUser: any;
onReply?: (commentId: string) => void;
}
export function CommentItem({ comment, owner, repo, pullNumber, currentUser, onReply }: CommentItemProps) {
const [isApplying, setIsApplying] = useState(false);
// Parse suggestion
const suggestionRegex = /```suggestion\n([\s\S]*?)```/;
const match = comment.body.match(suggestionRegex);
const hasSuggestion = !!match;
const suggestionContent = match ? match[1] : null;
// Text parts (before and after suggestion)
const parts = comment.body.split(suggestionRegex);
const beforeText = parts[0];
const afterText = parts[2] || ""; // parts[1] is the capturing group
const handleApply = async () => {
if (!confirm("Are you sure you want to apply this suggestion?")) return;
setIsApplying(true);
try {
const res = await fetch(`/api/repos/${owner}/${repo}/pulls/${pullNumber}/suggestions/apply`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
commentIds: [comment.id],
commitMessage: `Apply suggestion from @${comment.author.username}`
})
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.message || "Failed to apply suggestion");
}
toast.success("Suggestion applied!");
// Reload to show changes
setTimeout(() => window.location.reload(), 1000);
} catch (e: any) {
toast.error(e.message);
} finally {
setIsApplying(false);
}
};
return (
<div className="flex gap-3 py-4 border-b last:border-0">
{/* Avatar */}
<div className="flex-shrink-0">
{comment.author.avatarUrl ? (
<img src={comment.author.avatarUrl} alt={comment.author.username} className="h-8 w-8 rounded-full" />
) : (
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center font-medium">
{comment.author.username.charAt(0).toUpperCase()}
</div>
)}
</div>
<div className="flex-1 min-w-0 space-y-1">
<div className="flex items-center justify-between">
<div className="text-sm">
<span className="font-semibold text-foreground">{comment.author.displayName || comment.author.username}</span>
<span className="text-muted-foreground ml-2">
{new Date(comment.createdAt).toLocaleDateString()}
</span>
</div>
{/* Actions dropdown could go here */}
</div>
{/* Content */}
<div className="text-sm text-foreground/90 prose dark:prose-invert max-w-none">
{/* Before text */}
{beforeText && <p className="whitespace-pre-wrap">{beforeText}</p>}
{/* Suggestion Block */}
{hasSuggestion && suggestionContent && (
<div className="my-3 border rounded-md overflow-hidden">
<div className="bg-muted px-3 py-2 text-xs font-medium flex items-center justify-between">
<span>Suggested change</span>
<div className="flex items-center gap-2">
{comment.suggestionApplied ? (
<span className="flex items-center gap-1 text-green-600 bg-green-500/10 px-2 py-0.5 rounded-full border border-green-500/20">
<Check className="h-3 w-3" />
Applied
</span>
) : (
<Button
size="sm"
variant="outline"
className="h-7 text-xs bg-background"
onClick={handleApply}
disabled={isApplying}
>
{isApplying ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : <Copy className="h-3 w-3 mr-1" />}
Apply suggestion
</Button>
)}
</div>
</div>
<div className="bg-muted/30 p-0 overflow-x-auto">
<pre className="p-3 text-sm font-mono m-0 bg-transparent">{suggestionContent}</pre>
</div>
</div>
)}
{/* After text */}
{afterText && <p className="whitespace-pre-wrap">{afterText}</p>}
</div>
</div>
</div>
);
}