newpr 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -1
- package/src/analyzer/pipeline.ts +37 -15
- package/src/analyzer/progress.ts +2 -0
- package/src/cli/index.ts +7 -2
- package/src/cli/preflight.ts +126 -0
- package/src/github/fetch-pr.ts +53 -1
- package/src/history/store.ts +107 -1
- package/src/history/types.ts +1 -0
- package/src/llm/client.ts +197 -0
- package/src/llm/prompts.ts +80 -19
- package/src/llm/response-parser.ts +13 -1
- package/src/tui/Shell.tsx +7 -2
- package/src/types/github.ts +14 -0
- package/src/types/output.ts +50 -0
- package/src/web/client/App.tsx +33 -5
- package/src/web/client/components/AppShell.tsx +107 -47
- package/src/web/client/components/ChatSection.tsx +427 -0
- package/src/web/client/components/DetailPane.tsx +217 -77
- package/src/web/client/components/DiffViewer.tsx +713 -0
- package/src/web/client/components/InputScreen.tsx +178 -27
- package/src/web/client/components/LoadingTimeline.tsx +19 -6
- package/src/web/client/components/Markdown.tsx +220 -41
- package/src/web/client/components/ResultsScreen.tsx +109 -73
- package/src/web/client/components/ReviewModal.tsx +187 -0
- package/src/web/client/components/SettingsPanel.tsx +62 -86
- package/src/web/client/components/TipTapEditor.tsx +405 -0
- package/src/web/client/hooks/useAnalysis.ts +8 -1
- package/src/web/client/lib/shiki.ts +63 -0
- package/src/web/client/panels/CartoonPanel.tsx +94 -37
- package/src/web/client/panels/DiscussionPanel.tsx +158 -0
- package/src/web/client/panels/FilesPanel.tsx +435 -54
- package/src/web/client/panels/GroupsPanel.tsx +62 -40
- package/src/web/client/panels/StoryPanel.tsx +43 -23
- package/src/web/components/ui/tabs.tsx +3 -3
- package/src/web/server/routes.ts +856 -14
- package/src/web/server/session-manager.ts +11 -2
- package/src/web/server.ts +66 -4
- package/src/web/styles/built.css +1 -1
- package/src/web/styles/globals.css +117 -1
- package/src/workspace/agent.ts +22 -6
- package/src/workspace/explore.ts +41 -16
- package/src/web/client/panels/NarrativePanel.tsx +0 -9
- package/src/web/client/panels/SummaryPanel.tsx +0 -20
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef } from "react";
|
|
2
|
-
import { ArrowLeft,
|
|
3
|
-
import { Button } from "../../components/ui/button.tsx";
|
|
2
|
+
import { ArrowLeft, Layers, FolderTree, BookOpen, MessageSquare, GitBranch, Sparkles, Check, ChevronDown } from "lucide-react";
|
|
4
3
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../../components/ui/tabs.tsx";
|
|
5
4
|
import type { NewprOutput } from "../../../types/output.ts";
|
|
6
|
-
import { SummaryPanel } from "../panels/SummaryPanel.tsx";
|
|
7
5
|
import { GroupsPanel } from "../panels/GroupsPanel.tsx";
|
|
8
6
|
import { FilesPanel } from "../panels/FilesPanel.tsx";
|
|
9
|
-
import { NarrativePanel } from "../panels/NarrativePanel.tsx";
|
|
10
7
|
import { StoryPanel } from "../panels/StoryPanel.tsx";
|
|
8
|
+
import { DiscussionPanel } from "../panels/DiscussionPanel.tsx";
|
|
11
9
|
import { CartoonPanel } from "../panels/CartoonPanel.tsx";
|
|
10
|
+
import { ReviewModal } from "./ReviewModal.tsx";
|
|
12
11
|
|
|
13
|
-
const VALID_TABS = ["story", "
|
|
12
|
+
const VALID_TABS = ["story", "discussion", "groups", "files", "cartoon"] as const;
|
|
14
13
|
type TabValue = typeof VALID_TABS[number];
|
|
15
14
|
|
|
16
15
|
function getInitialTab(): TabValue {
|
|
@@ -25,11 +24,18 @@ function setTabParam(tab: string) {
|
|
|
25
24
|
window.history.replaceState(null, "", url.toString());
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
const
|
|
29
|
-
low: "bg-green-500
|
|
30
|
-
medium: "bg-yellow-500
|
|
31
|
-
high: "bg-red-500
|
|
32
|
-
critical: "bg-red-
|
|
27
|
+
const RISK_DOT: Record<string, string> = {
|
|
28
|
+
low: "bg-green-500",
|
|
29
|
+
medium: "bg-yellow-500",
|
|
30
|
+
high: "bg-red-500",
|
|
31
|
+
critical: "bg-red-600",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const STATE_STYLES: Record<string, { bg: string; text: string; label: string }> = {
|
|
35
|
+
open: { bg: "bg-green-500/10", text: "text-green-600 dark:text-green-400", label: "Open" },
|
|
36
|
+
merged: { bg: "bg-purple-500/10", text: "text-purple-600 dark:text-purple-400", label: "Merged" },
|
|
37
|
+
closed: { bg: "bg-red-500/10", text: "text-red-600 dark:text-red-400", label: "Closed" },
|
|
38
|
+
draft: { bg: "bg-neutral-500/10", text: "text-neutral-500", label: "Draft" },
|
|
33
39
|
};
|
|
34
40
|
|
|
35
41
|
export function ResultsScreen({
|
|
@@ -39,16 +45,19 @@ export function ResultsScreen({
|
|
|
39
45
|
onAnchorClick,
|
|
40
46
|
cartoonEnabled,
|
|
41
47
|
sessionId,
|
|
48
|
+
onTabChange,
|
|
42
49
|
}: {
|
|
43
50
|
data: NewprOutput;
|
|
44
51
|
onBack: () => void;
|
|
45
52
|
activeId: string | null;
|
|
46
|
-
onAnchorClick: (kind: "group" | "file", id: string) => void;
|
|
53
|
+
onAnchorClick: (kind: "group" | "file" | "line", id: string) => void;
|
|
47
54
|
cartoonEnabled?: boolean;
|
|
48
55
|
sessionId?: string | null;
|
|
56
|
+
onTabChange?: (tab: string) => void;
|
|
49
57
|
}) {
|
|
50
58
|
const { meta, summary } = data;
|
|
51
59
|
const [tab, setTab] = useState<TabValue>(getInitialTab);
|
|
60
|
+
const [reviewOpen, setReviewOpen] = useState(false);
|
|
52
61
|
|
|
53
62
|
const stickyRef = useRef<HTMLDivElement>(null);
|
|
54
63
|
const collapsibleRef = useRef<HTMLDivElement>(null);
|
|
@@ -84,106 +93,127 @@ export function ResultsScreen({
|
|
|
84
93
|
const handleTabChange = useCallback((value: string) => {
|
|
85
94
|
setTab(value as TabValue);
|
|
86
95
|
setTabParam(value);
|
|
87
|
-
|
|
96
|
+
onTabChange?.(value);
|
|
97
|
+
}, [onTabChange]);
|
|
88
98
|
|
|
89
99
|
const repoSlug = meta.pr_url.replace(/^https?:\/\/github\.com\//, "").replace(/\/pull\//, "#");
|
|
90
100
|
|
|
91
101
|
return (
|
|
102
|
+
<>
|
|
92
103
|
<Tabs value={tab} onValueChange={handleTabChange} className="flex flex-col">
|
|
93
|
-
<div ref={stickyRef} className="sticky top-0 z-10 bg-background
|
|
104
|
+
<div ref={stickyRef} className="sticky top-0 z-10 bg-background -mx-10 px-10">
|
|
94
105
|
<div ref={collapsibleRef} className="overflow-hidden transition-[max-height,opacity] duration-200">
|
|
95
|
-
<div className="pb-
|
|
96
|
-
<div className="flex items-center gap-
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
<div className="pb-4 pt-1">
|
|
107
|
+
<div className="flex items-center gap-2 mb-3">
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
onClick={onBack}
|
|
111
|
+
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground/40 hover:text-foreground hover:bg-accent/40 transition-colors shrink-0 -ml-1"
|
|
112
|
+
>
|
|
113
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
114
|
+
</button>
|
|
100
115
|
<a
|
|
101
116
|
href={meta.pr_url}
|
|
102
117
|
target="_blank"
|
|
103
118
|
rel="noopener noreferrer"
|
|
104
|
-
className="text-muted-foreground font-mono
|
|
119
|
+
className="text-[11px] text-muted-foreground/50 font-mono hover:text-foreground transition-colors"
|
|
105
120
|
>
|
|
106
121
|
{repoSlug}
|
|
107
122
|
</a>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
{meta.pr_state && (() => {
|
|
124
|
+
const s = STATE_STYLES[meta.pr_state] ?? STATE_STYLES.open!;
|
|
125
|
+
return (
|
|
126
|
+
<span className={`text-[10px] font-medium px-1.5 py-0.5 rounded-md ${s!.bg} ${s!.text}`}>
|
|
127
|
+
{s!.label}
|
|
128
|
+
</span>
|
|
129
|
+
);
|
|
130
|
+
})()}
|
|
131
|
+
<span className={`h-1.5 w-1.5 rounded-full shrink-0 ${RISK_DOT[summary.risk_level] ?? RISK_DOT.medium}`} />
|
|
132
|
+
<div className="flex-1" />
|
|
133
|
+
{meta.pr_state !== "merged" && meta.pr_state !== "closed" && (
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onClick={() => setReviewOpen(true)}
|
|
137
|
+
className="flex items-center gap-1.5 h-7 px-3 rounded-md border text-[11px] font-medium text-foreground hover:bg-accent/40 transition-colors shrink-0"
|
|
138
|
+
>
|
|
139
|
+
<Check className="h-3 w-3" />
|
|
140
|
+
Review
|
|
141
|
+
<ChevronDown className="h-3 w-3 text-muted-foreground/40" />
|
|
142
|
+
</button>
|
|
143
|
+
)}
|
|
111
144
|
</div>
|
|
112
145
|
|
|
113
|
-
<h1 className="text-
|
|
146
|
+
<h1 className="text-sm font-semibold tracking-tight mb-3 line-clamp-2">{meta.pr_title}</h1>
|
|
114
147
|
|
|
115
|
-
<div className="flex flex-wrap gap-x-
|
|
148
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 text-[11px] text-muted-foreground/50">
|
|
116
149
|
<a
|
|
117
150
|
href={meta.author_url ?? `https://github.com/${meta.author}`}
|
|
118
151
|
target="_blank"
|
|
119
152
|
rel="noopener noreferrer"
|
|
120
|
-
className="flex items-center gap-1.5
|
|
153
|
+
className="flex items-center gap-1.5 hover:text-foreground transition-colors"
|
|
121
154
|
>
|
|
122
|
-
{meta.author_avatar
|
|
123
|
-
<img src={meta.author_avatar} alt={meta.author} className="h-
|
|
124
|
-
) : (
|
|
125
|
-
<User className="h-3 w-3" />
|
|
155
|
+
{meta.author_avatar && (
|
|
156
|
+
<img src={meta.author_avatar} alt={meta.author} className="h-4 w-4 rounded-full" />
|
|
126
157
|
)}
|
|
127
158
|
<span>{meta.author}</span>
|
|
128
159
|
</a>
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
<
|
|
132
|
-
<span className="text-muted-foreground/50">←</span>
|
|
160
|
+
<span className="text-muted-foreground/15">|</span>
|
|
161
|
+
<div className="flex items-center gap-1">
|
|
162
|
+
<GitBranch className="h-3 w-3 text-muted-foreground/30" />
|
|
133
163
|
<span className="font-mono">{meta.head_branch}</span>
|
|
164
|
+
<span className="text-muted-foreground/25">→</span>
|
|
165
|
+
<span className="font-mono">{meta.base_branch}</span>
|
|
134
166
|
</div>
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
<span className="text-green-
|
|
138
|
-
<span className="text-red-
|
|
139
|
-
<span className="text-muted-foreground/
|
|
140
|
-
<span>{meta.total_files_changed} files</span>
|
|
141
|
-
</div>
|
|
142
|
-
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
143
|
-
<Bot className="h-3 w-3" />
|
|
144
|
-
<span>{meta.model_used.split("/").pop()}</span>
|
|
167
|
+
<span className="text-muted-foreground/15">|</span>
|
|
168
|
+
<div className="flex items-center gap-1.5">
|
|
169
|
+
<span className="text-green-600 dark:text-green-400 tabular-nums">+{meta.total_additions}</span>
|
|
170
|
+
<span className="text-red-600 dark:text-red-400 tabular-nums">-{meta.total_deletions}</span>
|
|
171
|
+
<span className="text-muted-foreground/25">·</span>
|
|
172
|
+
<span className="tabular-nums">{meta.total_files_changed} files</span>
|
|
145
173
|
</div>
|
|
146
174
|
</div>
|
|
147
175
|
</div>
|
|
148
176
|
</div>
|
|
149
177
|
|
|
150
178
|
<div ref={compactRef} className="overflow-hidden transition-[max-height,opacity] duration-200" style={{ maxHeight: 0, opacity: 0 }}>
|
|
151
|
-
<div className="flex items-center gap-
|
|
152
|
-
<
|
|
179
|
+
<div className="flex items-center gap-2.5 min-w-0 pb-2.5">
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
onClick={onBack}
|
|
183
|
+
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground/40 hover:text-foreground hover:bg-accent/40 transition-colors shrink-0 -ml-1"
|
|
184
|
+
>
|
|
153
185
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
154
|
-
</
|
|
155
|
-
<span className=
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
{
|
|
159
|
-
|
|
186
|
+
</button>
|
|
187
|
+
<span className={`h-1.5 w-1.5 rounded-full shrink-0 ${RISK_DOT[summary.risk_level] ?? RISK_DOT.medium}`} />
|
|
188
|
+
{meta.pr_state && (() => {
|
|
189
|
+
const s = STATE_STYLES[meta.pr_state]!;
|
|
190
|
+
return <span className={`text-[9px] font-medium px-1 py-px rounded ${s.bg} ${s.text} shrink-0`}>{s.label}</span>;
|
|
191
|
+
})()}
|
|
192
|
+
<span className="text-xs font-medium truncate flex-1">{meta.pr_title}</span>
|
|
193
|
+
<span className="text-[10px] text-muted-foreground/30 font-mono shrink-0">{repoSlug}</span>
|
|
160
194
|
</div>
|
|
161
195
|
</div>
|
|
162
196
|
|
|
163
|
-
<TabsList className="w-full justify-start
|
|
164
|
-
<TabsTrigger value="story"
|
|
165
|
-
<BookOpen className="h-3
|
|
197
|
+
<TabsList className="w-full justify-start">
|
|
198
|
+
<TabsTrigger value="story">
|
|
199
|
+
<BookOpen className="h-3 w-3 shrink-0" />
|
|
166
200
|
Story
|
|
167
201
|
</TabsTrigger>
|
|
168
|
-
<TabsTrigger value="
|
|
169
|
-
<
|
|
170
|
-
|
|
202
|
+
<TabsTrigger value="discussion">
|
|
203
|
+
<MessageSquare className="h-3 w-3 shrink-0" />
|
|
204
|
+
Discussion
|
|
171
205
|
</TabsTrigger>
|
|
172
|
-
<TabsTrigger value="groups"
|
|
173
|
-
<Layers className="h-3
|
|
206
|
+
<TabsTrigger value="groups">
|
|
207
|
+
<Layers className="h-3 w-3 shrink-0" />
|
|
174
208
|
Groups
|
|
175
209
|
</TabsTrigger>
|
|
176
|
-
<TabsTrigger value="files"
|
|
177
|
-
<FolderTree className="h-3
|
|
210
|
+
<TabsTrigger value="files">
|
|
211
|
+
<FolderTree className="h-3 w-3 shrink-0" />
|
|
178
212
|
Files
|
|
179
213
|
</TabsTrigger>
|
|
180
|
-
<TabsTrigger value="narrative" className="gap-1.5">
|
|
181
|
-
<FileText className="h-3.5 w-3.5 shrink-0" />
|
|
182
|
-
Narrative
|
|
183
|
-
</TabsTrigger>
|
|
184
214
|
{cartoonEnabled && (
|
|
185
|
-
<TabsTrigger value="cartoon"
|
|
186
|
-
<Sparkles className="h-3
|
|
215
|
+
<TabsTrigger value="cartoon">
|
|
216
|
+
<Sparkles className="h-3 w-3 shrink-0" />
|
|
187
217
|
Comic
|
|
188
218
|
</TabsTrigger>
|
|
189
219
|
)}
|
|
@@ -193,17 +223,19 @@ export function ResultsScreen({
|
|
|
193
223
|
<TabsContent value="story">
|
|
194
224
|
<StoryPanel data={data} activeId={activeId} onAnchorClick={onAnchorClick} />
|
|
195
225
|
</TabsContent>
|
|
196
|
-
<TabsContent value="
|
|
197
|
-
<
|
|
226
|
+
<TabsContent value="discussion">
|
|
227
|
+
<DiscussionPanel sessionId={sessionId} />
|
|
198
228
|
</TabsContent>
|
|
199
229
|
<TabsContent value="groups">
|
|
200
230
|
<GroupsPanel groups={data.groups} />
|
|
201
231
|
</TabsContent>
|
|
202
232
|
<TabsContent value="files">
|
|
203
|
-
<FilesPanel
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
233
|
+
<FilesPanel
|
|
234
|
+
files={data.files}
|
|
235
|
+
groups={data.groups}
|
|
236
|
+
selectedPath={activeId?.startsWith("file:") ? activeId.slice(5) : null}
|
|
237
|
+
onFileSelect={(path: string) => onAnchorClick("file", path)}
|
|
238
|
+
/>
|
|
207
239
|
</TabsContent>
|
|
208
240
|
{cartoonEnabled && (
|
|
209
241
|
<TabsContent value="cartoon">
|
|
@@ -211,5 +243,9 @@ export function ResultsScreen({
|
|
|
211
243
|
</TabsContent>
|
|
212
244
|
)}
|
|
213
245
|
</Tabs>
|
|
246
|
+
{reviewOpen && (
|
|
247
|
+
<ReviewModal prUrl={meta.pr_url} onClose={() => setReviewOpen(false)} />
|
|
248
|
+
)}
|
|
249
|
+
</>
|
|
214
250
|
);
|
|
215
251
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from "react";
|
|
2
|
+
import { X, Check, MessageSquare, Loader2, AlertCircle, ExternalLink } from "lucide-react";
|
|
3
|
+
import { TipTapEditor, getTextWithAnchors } from "./TipTapEditor.tsx";
|
|
4
|
+
import type { useEditor } from "@tiptap/react";
|
|
5
|
+
|
|
6
|
+
type ReviewEvent = "APPROVE" | "REQUEST_CHANGES" | "COMMENT";
|
|
7
|
+
|
|
8
|
+
const EVENTS: { value: ReviewEvent; label: string; description: string; class: string; activeClass: string }[] = [
|
|
9
|
+
{
|
|
10
|
+
value: "APPROVE",
|
|
11
|
+
label: "Approve",
|
|
12
|
+
description: "Submit approval for this PR",
|
|
13
|
+
class: "text-green-600 dark:text-green-400 hover:bg-green-500/10",
|
|
14
|
+
activeClass: "bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/30",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
value: "REQUEST_CHANGES",
|
|
18
|
+
label: "Request changes",
|
|
19
|
+
description: "Submit feedback that must be addressed",
|
|
20
|
+
class: "text-red-600 dark:text-red-400 hover:bg-red-500/10",
|
|
21
|
+
activeClass: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/30",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
value: "COMMENT",
|
|
25
|
+
label: "Comment",
|
|
26
|
+
description: "Submit general feedback",
|
|
27
|
+
class: "text-muted-foreground hover:bg-accent/50",
|
|
28
|
+
activeClass: "bg-accent text-foreground border-border",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
interface ReviewModalProps {
|
|
33
|
+
prUrl: string;
|
|
34
|
+
onClose: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
|
|
38
|
+
const [event, setEvent] = useState<ReviewEvent>("APPROVE");
|
|
39
|
+
const [submitting, setSubmitting] = useState(false);
|
|
40
|
+
const [result, setResult] = useState<{ ok: boolean; html_url?: string; error?: string } | null>(null);
|
|
41
|
+
const editorRef = useRef<ReturnType<typeof useEditor>>(null);
|
|
42
|
+
|
|
43
|
+
const handleSubmit = useCallback(async () => {
|
|
44
|
+
if (submitting) return;
|
|
45
|
+
setSubmitting(true);
|
|
46
|
+
setResult(null);
|
|
47
|
+
try {
|
|
48
|
+
const body = editorRef.current ? getTextWithAnchors(editorRef.current) : "";
|
|
49
|
+
const res = await fetch("/api/review", {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ pr_url: prUrl, event, body }),
|
|
53
|
+
});
|
|
54
|
+
const data = await res.json() as { ok?: boolean; html_url?: string; error?: string };
|
|
55
|
+
if (data.ok) {
|
|
56
|
+
setResult({ ok: true, html_url: data.html_url });
|
|
57
|
+
} else {
|
|
58
|
+
setResult({ ok: false, error: data.error ?? "Failed to submit review" });
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
setResult({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
62
|
+
} finally {
|
|
63
|
+
setSubmitting(false);
|
|
64
|
+
}
|
|
65
|
+
}, [prUrl, event, submitting]);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh]" onClick={onClose}>
|
|
69
|
+
<div className="fixed inset-0 bg-background/60 backdrop-blur-sm" />
|
|
70
|
+
<div
|
|
71
|
+
className="relative z-10 w-full max-w-md rounded-xl border bg-background shadow-lg"
|
|
72
|
+
onClick={(e) => e.stopPropagation()}
|
|
73
|
+
>
|
|
74
|
+
<div className="flex items-center justify-between px-4 h-11 border-b">
|
|
75
|
+
<span className="text-xs font-medium">Submit Review</span>
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={onClose}
|
|
79
|
+
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground/40 hover:text-foreground hover:bg-accent/40 transition-colors"
|
|
80
|
+
>
|
|
81
|
+
<X className="h-3.5 w-3.5" />
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="px-4 py-4 space-y-4">
|
|
86
|
+
{result?.ok ? (
|
|
87
|
+
<div className="space-y-4 py-2">
|
|
88
|
+
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
|
89
|
+
<Check className="h-4 w-4" />
|
|
90
|
+
<span className="text-xs font-medium">Review submitted</span>
|
|
91
|
+
</div>
|
|
92
|
+
{result.html_url && (
|
|
93
|
+
<a
|
|
94
|
+
href={result.html_url}
|
|
95
|
+
target="_blank"
|
|
96
|
+
rel="noopener noreferrer"
|
|
97
|
+
className="inline-flex items-center gap-1.5 text-[11px] text-muted-foreground/60 hover:text-foreground transition-colors"
|
|
98
|
+
>
|
|
99
|
+
<ExternalLink className="h-3 w-3" />
|
|
100
|
+
View on GitHub
|
|
101
|
+
</a>
|
|
102
|
+
)}
|
|
103
|
+
<div className="flex justify-end">
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
onClick={onClose}
|
|
107
|
+
className="text-[11px] text-muted-foreground/50 hover:text-foreground px-3 py-1.5 rounded-md hover:bg-accent/40 transition-colors"
|
|
108
|
+
>
|
|
109
|
+
Close
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
) : (
|
|
114
|
+
<>
|
|
115
|
+
<div className="flex gap-1.5 p-0.5 rounded-lg border">
|
|
116
|
+
{EVENTS.map((e) => (
|
|
117
|
+
<button
|
|
118
|
+
key={e.value}
|
|
119
|
+
type="button"
|
|
120
|
+
onClick={() => setEvent(e.value)}
|
|
121
|
+
className={`flex-1 text-[11px] font-medium px-2 py-1.5 rounded-md transition-colors border border-transparent ${
|
|
122
|
+
event === e.value ? e.activeClass : e.class
|
|
123
|
+
}`}
|
|
124
|
+
>
|
|
125
|
+
{e.label}
|
|
126
|
+
</button>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div>
|
|
131
|
+
<div className="text-[10px] font-medium text-muted-foreground/40 uppercase tracking-wider mb-2">
|
|
132
|
+
Message {event !== "APPROVE" && <span className="text-red-500/60 normal-case">*</span>}
|
|
133
|
+
</div>
|
|
134
|
+
<div className="rounded-lg border px-3 py-2.5 min-h-[80px] focus-within:border-foreground/15 transition-colors">
|
|
135
|
+
<TipTapEditor
|
|
136
|
+
editorRef={editorRef}
|
|
137
|
+
placeholder={event === "APPROVE" ? "Optional message..." : "Describe the changes needed..."}
|
|
138
|
+
autoFocus
|
|
139
|
+
submitOnModEnter
|
|
140
|
+
onSubmit={handleSubmit}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{result?.error && (
|
|
146
|
+
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-red-500/5 border border-red-500/20">
|
|
147
|
+
<AlertCircle className="h-3.5 w-3.5 text-red-500 shrink-0 mt-0.5" />
|
|
148
|
+
<p className="text-[11px] text-red-600 dark:text-red-400">{result.error}</p>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
<div className="flex items-center justify-between pt-1">
|
|
153
|
+
<span className="text-[10px] text-muted-foreground/25">
|
|
154
|
+
{navigator.platform.includes("Mac") ? "⌘" : "Ctrl"}+Enter to submit
|
|
155
|
+
</span>
|
|
156
|
+
<div className="flex gap-2">
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
onClick={onClose}
|
|
160
|
+
className="text-[11px] text-muted-foreground/50 hover:text-foreground px-3 py-1.5 rounded-md hover:bg-accent/40 transition-colors"
|
|
161
|
+
>
|
|
162
|
+
Cancel
|
|
163
|
+
</button>
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onClick={handleSubmit}
|
|
167
|
+
disabled={submitting}
|
|
168
|
+
className="flex items-center gap-1.5 text-[11px] font-medium px-3 py-1.5 rounded-md bg-foreground text-background hover:opacity-80 disabled:opacity-30 transition-opacity"
|
|
169
|
+
>
|
|
170
|
+
{submitting ? (
|
|
171
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
172
|
+
) : event === "APPROVE" ? (
|
|
173
|
+
<Check className="h-3 w-3" />
|
|
174
|
+
) : (
|
|
175
|
+
<MessageSquare className="h-3 w-3" />
|
|
176
|
+
)}
|
|
177
|
+
Submit
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|