newpr 1.0.26 → 1.0.28
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 +1 -1
- package/src/analyzer/pipeline.ts +1 -0
- package/src/config/index.ts +1 -0
- package/src/config/store.ts +1 -0
- package/src/llm/prompts.ts +10 -4
- package/src/types/config.ts +1 -0
- package/src/web/client/components/AppShell.tsx +29 -22
- package/src/web/client/components/ChatSection.tsx +18 -12
- package/src/web/client/components/DetailPane.tsx +10 -6
- package/src/web/client/components/ErrorScreen.tsx +15 -41
- package/src/web/client/components/FeasibilityAlert.tsx +5 -3
- package/src/web/client/components/InputScreen.tsx +21 -17
- package/src/web/client/components/LoadingTimeline.tsx +22 -17
- package/src/web/client/components/ResultsScreen.tsx +31 -20
- package/src/web/client/components/ReviewModal.tsx +23 -15
- package/src/web/client/components/SettingsPanel.tsx +100 -25
- package/src/web/client/lib/i18n/context.tsx +76 -0
- package/src/web/client/lib/i18n/en.ts +276 -0
- package/src/web/client/lib/i18n/index.ts +3 -0
- package/src/web/client/lib/i18n/ko.ts +274 -0
- package/src/web/client/main.tsx +4 -1
- package/src/web/client/panels/CartoonPanel.tsx +12 -10
- package/src/web/client/panels/DiscussionPanel.tsx +14 -12
- package/src/web/client/panels/FilesPanel.tsx +14 -9
- package/src/web/client/panels/GroupsPanel.tsx +3 -1
- package/src/web/client/panels/SlidesPanel.tsx +17 -15
- package/src/web/client/panels/StackPanel.tsx +50 -44
- package/src/web/client/panels/StoryPanel.tsx +5 -3
- package/src/web/server/routes.ts +27 -21
- package/src/web/styles/built.css +1 -1
|
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
|
|
|
2
2
|
import { CornerDownLeft, GitPullRequest, ExternalLink, ChevronUp } from "lucide-react";
|
|
3
3
|
import type { SessionRecord } from "../../../history/types.ts";
|
|
4
4
|
import { analytics } from "../lib/analytics.ts";
|
|
5
|
+
import { useI18n, type TranslationKey } from "../lib/i18n/index.ts";
|
|
5
6
|
|
|
6
7
|
interface ToolStatus {
|
|
7
8
|
name: string;
|
|
@@ -23,16 +24,16 @@ const RISK_DOT: Record<string, string> = {
|
|
|
23
24
|
critical: "bg-red-600",
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
function timeAgo(date: string): string {
|
|
27
|
+
function timeAgo(date: string, t: (key: TranslationKey, params?: Record<string, string | number>) => string): string {
|
|
27
28
|
const s = Math.floor((Date.now() - new Date(date).getTime()) / 1000);
|
|
28
|
-
if (s < 60) return "
|
|
29
|
+
if (s < 60) return t("time.justNow");
|
|
29
30
|
const m = Math.floor(s / 60);
|
|
30
|
-
if (m < 60) return
|
|
31
|
+
if (m < 60) return t("time.minutesAgo", { n: m });
|
|
31
32
|
const h = Math.floor(m / 60);
|
|
32
|
-
if (h < 24) return
|
|
33
|
+
if (h < 24) return t("time.hoursAgo", { n: h });
|
|
33
34
|
const d = Math.floor(h / 24);
|
|
34
|
-
if (d < 30) return
|
|
35
|
-
return
|
|
35
|
+
if (d < 30) return t("time.daysAgo", { n: d });
|
|
36
|
+
return t("time.monthsAgo", { n: Math.floor(d / 30) });
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
function StatusDot({ ok, optional }: { ok: boolean; optional?: boolean }) {
|
|
@@ -42,6 +43,7 @@ function StatusDot({ ok, optional }: { ok: boolean; optional?: boolean }) {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function CompactStatus({ data }: { data: PreflightData }) {
|
|
46
|
+
const { t } = useI18n();
|
|
45
47
|
const [open, setOpen] = useState(false);
|
|
46
48
|
const gh = data.github;
|
|
47
49
|
const allOk = gh.installed && gh.authenticated && data.openrouterKey;
|
|
@@ -53,7 +55,7 @@ function CompactStatus({ data }: { data: PreflightData }) {
|
|
|
53
55
|
className="flex items-center gap-1.5 hover:opacity-70 transition-opacity"
|
|
54
56
|
>
|
|
55
57
|
<StatusDot ok={allOk} />
|
|
56
|
-
<span className="text-[10px] font-mono text-muted-foreground/30">status</span>
|
|
58
|
+
<span className="text-[10px] font-mono text-muted-foreground/30">{t("input.status")}</span>
|
|
57
59
|
<ChevronUp className={`h-2.5 w-2.5 text-muted-foreground/20 transition-transform ${open ? "" : "rotate-180"}`} />
|
|
58
60
|
</button>
|
|
59
61
|
{open && (
|
|
@@ -94,6 +96,7 @@ export function InputScreen({
|
|
|
94
96
|
onSessionSelect?: (id: string) => void;
|
|
95
97
|
version?: string;
|
|
96
98
|
}) {
|
|
99
|
+
const { t } = useI18n();
|
|
97
100
|
const [value, setValue] = useState("");
|
|
98
101
|
const [focused, setFocused] = useState(false);
|
|
99
102
|
const [preflight, setPreflight] = useState<PreflightData | null>(null);
|
|
@@ -126,9 +129,9 @@ export function InputScreen({
|
|
|
126
129
|
</span>
|
|
127
130
|
)}
|
|
128
131
|
</div>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
<p className="text-base text-muted-foreground/50">
|
|
133
|
+
{t("input.tagline")}
|
|
134
|
+
</p>
|
|
132
135
|
</div>
|
|
133
136
|
|
|
134
137
|
<div>
|
|
@@ -157,7 +160,7 @@ export function InputScreen({
|
|
|
157
160
|
</div>
|
|
158
161
|
<div className="flex justify-center mt-2.5">
|
|
159
162
|
<span className="text-[10px] text-muted-foreground/20 font-mono">
|
|
160
|
-
|
|
163
|
+
{t("input.enterToAnalyze")}
|
|
161
164
|
</span>
|
|
162
165
|
</div>
|
|
163
166
|
</form>
|
|
@@ -168,9 +171,9 @@ export function InputScreen({
|
|
|
168
171
|
|
|
169
172
|
{recents.length > 0 && (
|
|
170
173
|
<div className="space-y-3">
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
<div className="text-[10px] font-medium text-muted-foreground/25 uppercase tracking-[0.15em] text-center">
|
|
175
|
+
{t("input.recent")}
|
|
176
|
+
</div>
|
|
174
177
|
<div className="space-y-px">
|
|
175
178
|
{recents.map((s) => (
|
|
176
179
|
<button
|
|
@@ -189,7 +192,7 @@ export function InputScreen({
|
|
|
189
192
|
<span className="font-mono">{s.repo.split("/").pop()}</span>
|
|
190
193
|
<span className="font-mono">#{s.pr_number}</span>
|
|
191
194
|
<span className="text-muted-foreground/15">·</span>
|
|
192
|
-
<span>{timeAgo(s.analyzed_at)}</span>
|
|
195
|
+
<span>{timeAgo(s.analyzed_at, t)}</span>
|
|
193
196
|
</div>
|
|
194
197
|
</button>
|
|
195
198
|
))}
|
|
@@ -211,6 +214,7 @@ export function InputScreen({
|
|
|
211
214
|
const SIONIC_HERO_BG = "https://www.sionic.ai/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmain-intro-bg.1455295d.png&w=1920&q=75";
|
|
212
215
|
|
|
213
216
|
function SponsorBanner() {
|
|
217
|
+
const { t } = useI18n();
|
|
214
218
|
return (
|
|
215
219
|
<a
|
|
216
220
|
href="https://www.sionic.ai"
|
|
@@ -234,11 +238,11 @@ function SponsorBanner() {
|
|
|
234
238
|
/>
|
|
235
239
|
<div className="h-3 w-px bg-white/15 shrink-0" />
|
|
236
240
|
<span className="text-[10px] text-white/45 truncate">
|
|
237
|
-
|
|
241
|
+
{t("input.sponsorTagline")}
|
|
238
242
|
</span>
|
|
239
243
|
</div>
|
|
240
244
|
<div className="relative flex items-center gap-1.5 shrink-0">
|
|
241
|
-
<span className="text-[8px] text-white/20 uppercase tracking-widest">
|
|
245
|
+
<span className="text-[8px] text-white/20 uppercase tracking-widest">{t("input.ad")}</span>
|
|
242
246
|
<ExternalLink className="h-2.5 w-2.5 text-white/15 group-hover:text-white/40 transition-colors" />
|
|
243
247
|
</div>
|
|
244
248
|
</a>
|
|
@@ -2,18 +2,19 @@ import { useState, useEffect, useRef } from "react";
|
|
|
2
2
|
import { CheckCircle2, Circle, Loader2 } from "lucide-react";
|
|
3
3
|
import type { ProgressEvent, ProgressStage } from "../../../analyzer/progress.ts";
|
|
4
4
|
import { stageIndex, allStages } from "../../../analyzer/progress.ts";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
import { useI18n, type TranslationKey } from "../lib/i18n/index.ts";
|
|
6
|
+
|
|
7
|
+
const STAGE_KEYS: Record<ProgressStage, TranslationKey> = {
|
|
8
|
+
fetching: "loading.fetching",
|
|
9
|
+
parsing: "loading.parsing",
|
|
10
|
+
cloning: "loading.cloning",
|
|
11
|
+
checkout: "loading.checkout",
|
|
12
|
+
exploring: "loading.exploring",
|
|
13
|
+
analyzing: "loading.analyzing",
|
|
14
|
+
grouping: "loading.grouping",
|
|
15
|
+
summarizing: "loading.summarizing",
|
|
16
|
+
narrating: "loading.narrating",
|
|
17
|
+
done: "loading.done",
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
const MAX_LOG_LINES = 8;
|
|
@@ -29,7 +30,7 @@ interface StepInfo {
|
|
|
29
30
|
log: string[];
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function buildSteps(events: ProgressEvent[]): StepInfo[] {
|
|
33
|
+
function buildSteps(events: ProgressEvent[], stageLabels: Record<ProgressStage, string>): StepInfo[] {
|
|
33
34
|
const stages = allStages();
|
|
34
35
|
const lastByStage = new Map<ProgressStage, ProgressEvent>();
|
|
35
36
|
const firstTs = new Map<ProgressStage, number>();
|
|
@@ -72,7 +73,7 @@ function buildSteps(events: ProgressEvent[]): StepInfo[] {
|
|
|
72
73
|
|
|
73
74
|
return {
|
|
74
75
|
stage,
|
|
75
|
-
message: event?.message ??
|
|
76
|
+
message: event?.message ?? stageLabels[stage],
|
|
76
77
|
done,
|
|
77
78
|
active,
|
|
78
79
|
durationMs,
|
|
@@ -107,7 +108,11 @@ export function LoadingTimeline({
|
|
|
107
108
|
logEndRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
108
109
|
}, [events.length]);
|
|
109
110
|
|
|
110
|
-
const
|
|
111
|
+
const { t } = useI18n();
|
|
112
|
+
const stageLabels = Object.fromEntries(
|
|
113
|
+
Object.entries(STAGE_KEYS).map(([stage, key]) => [stage, t(key)]),
|
|
114
|
+
) as Record<ProgressStage, string>;
|
|
115
|
+
const steps = buildSteps(events, stageLabels);
|
|
111
116
|
const seconds = Math.floor(elapsed / 1000);
|
|
112
117
|
|
|
113
118
|
const prInfo = events.find((e) => e.pr_title);
|
|
@@ -136,7 +141,7 @@ export function LoadingTimeline({
|
|
|
136
141
|
|
|
137
142
|
<div className="space-y-3">
|
|
138
143
|
{steps.map((step) => {
|
|
139
|
-
const completionDetail = step.message !==
|
|
144
|
+
const completionDetail = step.message !== stageLabels[step.stage] ? step.message : "";
|
|
140
145
|
const progress = step.current !== undefined && step.total !== undefined
|
|
141
146
|
? ` (${step.current}/${step.total})`
|
|
142
147
|
: "";
|
|
@@ -154,7 +159,7 @@ export function LoadingTimeline({
|
|
|
154
159
|
<div className="min-w-0 flex-1">
|
|
155
160
|
<div className="flex items-center gap-2">
|
|
156
161
|
<span className={`text-base font-medium ${step.done ? "text-muted-foreground" : step.active ? "text-foreground" : "text-muted-foreground/50"}`}>
|
|
157
|
-
{
|
|
162
|
+
{stageLabels[step.stage]}{progress}
|
|
158
163
|
</span>
|
|
159
164
|
{step.done && step.durationMs !== undefined && (
|
|
160
165
|
<span className="text-sm text-muted-foreground/60">
|
|
@@ -11,6 +11,7 @@ import { SlidesPanel } from "../panels/SlidesPanel.tsx";
|
|
|
11
11
|
import { StackPanel } from "../panels/StackPanel.tsx";
|
|
12
12
|
import { ReviewModal } from "./ReviewModal.tsx";
|
|
13
13
|
import { useOutdatedCheck } from "../hooks/useOutdatedCheck.ts";
|
|
14
|
+
import { useI18n, type TranslationKey } from "../lib/i18n/index.ts";
|
|
14
15
|
|
|
15
16
|
const VALID_TABS = ["story", "discussion", "groups", "files", "stack", "slides", "cartoon"] as const;
|
|
16
17
|
type TabValue = typeof VALID_TABS[number];
|
|
@@ -34,11 +35,18 @@ const RISK_DOT: Record<string, string> = {
|
|
|
34
35
|
critical: "bg-red-600",
|
|
35
36
|
};
|
|
36
37
|
|
|
37
|
-
const
|
|
38
|
-
open: { bg: "bg-green-500/10", text: "text-green-600 dark:text-green-400"
|
|
39
|
-
merged: { bg: "bg-purple-500/10", text: "text-purple-600 dark:text-purple-400"
|
|
40
|
-
closed: { bg: "bg-red-500/10", text: "text-red-600 dark:text-red-400"
|
|
41
|
-
draft: { bg: "bg-neutral-500/10", text: "text-neutral-500"
|
|
38
|
+
const STATE_STYLE_CLASS: Record<string, { bg: string; text: string }> = {
|
|
39
|
+
open: { bg: "bg-green-500/10", text: "text-green-600 dark:text-green-400" },
|
|
40
|
+
merged: { bg: "bg-purple-500/10", text: "text-purple-600 dark:text-purple-400" },
|
|
41
|
+
closed: { bg: "bg-red-500/10", text: "text-red-600 dark:text-red-400" },
|
|
42
|
+
draft: { bg: "bg-neutral-500/10", text: "text-neutral-500" },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const STATE_LABEL_KEYS: Record<string, TranslationKey> = {
|
|
46
|
+
open: "results.open",
|
|
47
|
+
merged: "results.merged",
|
|
48
|
+
closed: "results.closed",
|
|
49
|
+
draft: "results.draft",
|
|
42
50
|
};
|
|
43
51
|
|
|
44
52
|
export function ResultsScreen({
|
|
@@ -68,6 +76,7 @@ export function ResultsScreen({
|
|
|
68
76
|
const [tab, setTab] = useState<TabValue>(getInitialTab);
|
|
69
77
|
const [reviewOpen, setReviewOpen] = useState(false);
|
|
70
78
|
const outdated = useOutdatedCheck(sessionId);
|
|
79
|
+
const { t } = useI18n();
|
|
71
80
|
|
|
72
81
|
useEffect(() => {
|
|
73
82
|
onTabChange?.(tab);
|
|
@@ -135,10 +144,11 @@ export function ResultsScreen({
|
|
|
135
144
|
{repoSlug}
|
|
136
145
|
</a>
|
|
137
146
|
{meta.pr_state && (() => {
|
|
138
|
-
const s =
|
|
147
|
+
const s = STATE_STYLE_CLASS[meta.pr_state] ?? STATE_STYLE_CLASS.open!;
|
|
148
|
+
const labelKey = STATE_LABEL_KEYS[meta.pr_state] ?? STATE_LABEL_KEYS.open!;
|
|
139
149
|
return (
|
|
140
150
|
<span className={`text-[10px] font-medium px-1.5 py-0.5 rounded-md ${s!.bg} ${s!.text}`}>
|
|
141
|
-
{
|
|
151
|
+
{t(labelKey)}
|
|
142
152
|
</span>
|
|
143
153
|
);
|
|
144
154
|
})()}
|
|
@@ -151,7 +161,7 @@ export function ResultsScreen({
|
|
|
151
161
|
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"
|
|
152
162
|
>
|
|
153
163
|
<Check className="h-3 w-3" />
|
|
154
|
-
|
|
164
|
+
{t("results.review")}
|
|
155
165
|
<ChevronDown className="h-3 w-3 text-muted-foreground/40" />
|
|
156
166
|
</button>
|
|
157
167
|
)}
|
|
@@ -183,7 +193,7 @@ export function ResultsScreen({
|
|
|
183
193
|
<span className="text-green-600 dark:text-green-400 tabular-nums">+{meta.total_additions}</span>
|
|
184
194
|
<span className="text-red-600 dark:text-red-400 tabular-nums">-{meta.total_deletions}</span>
|
|
185
195
|
<span className="text-muted-foreground/25">·</span>
|
|
186
|
-
<span className="tabular-nums">{meta.total_files_changed}
|
|
196
|
+
<span className="tabular-nums">{t("results.nFiles", { n: meta.total_files_changed })}</span>
|
|
187
197
|
</div>
|
|
188
198
|
</div>
|
|
189
199
|
</div>
|
|
@@ -191,7 +201,7 @@ export function ResultsScreen({
|
|
|
191
201
|
<div className="flex items-center gap-2 mt-3 px-3 py-2 rounded-lg border border-yellow-500/20 bg-yellow-500/5">
|
|
192
202
|
<AlertTriangle className="h-3.5 w-3.5 text-yellow-600 dark:text-yellow-400 shrink-0" />
|
|
193
203
|
<span className="text-[11px] text-yellow-700 dark:text-yellow-300 flex-1">
|
|
194
|
-
|
|
204
|
+
{t("results.prUpdated")}
|
|
195
205
|
</span>
|
|
196
206
|
{onReanalyze && (
|
|
197
207
|
<button
|
|
@@ -200,7 +210,7 @@ export function ResultsScreen({
|
|
|
200
210
|
className="flex items-center gap-1 text-[11px] font-medium text-yellow-700 dark:text-yellow-300 hover:text-yellow-900 dark:hover:text-yellow-100 shrink-0 transition-colors"
|
|
201
211
|
>
|
|
202
212
|
<RefreshCw className="h-3 w-3" />
|
|
203
|
-
|
|
213
|
+
{t("results.reAnalyze")}
|
|
204
214
|
</button>
|
|
205
215
|
)}
|
|
206
216
|
</div>
|
|
@@ -218,8 +228,9 @@ export function ResultsScreen({
|
|
|
218
228
|
</button>
|
|
219
229
|
<span className={`h-1.5 w-1.5 rounded-full shrink-0 ${RISK_DOT[summary.risk_level] ?? RISK_DOT.medium}`} />
|
|
220
230
|
{meta.pr_state && (() => {
|
|
221
|
-
const s =
|
|
222
|
-
|
|
231
|
+
const s = STATE_STYLE_CLASS[meta.pr_state]!;
|
|
232
|
+
const labelKey = STATE_LABEL_KEYS[meta.pr_state] ?? STATE_LABEL_KEYS.open!;
|
|
233
|
+
return <span className={`text-[9px] font-medium px-1 py-px rounded ${s.bg} ${s.text} shrink-0`}>{t(labelKey)}</span>;
|
|
223
234
|
})()}
|
|
224
235
|
<span className="text-sm font-medium truncate flex-1">{meta.pr_title}</span>
|
|
225
236
|
<span className="text-xs text-muted-foreground/30 font-mono shrink-0">{repoSlug}</span>
|
|
@@ -229,34 +240,34 @@ export function ResultsScreen({
|
|
|
229
240
|
<TabsList className="w-full justify-start">
|
|
230
241
|
<TabsTrigger value="story">
|
|
231
242
|
<BookOpen className="h-3 w-3 shrink-0" />
|
|
232
|
-
|
|
243
|
+
{t("results.story")}
|
|
233
244
|
</TabsTrigger>
|
|
234
245
|
<TabsTrigger value="discussion">
|
|
235
246
|
<MessageSquare className="h-3 w-3 shrink-0" />
|
|
236
|
-
|
|
247
|
+
{t("results.discussion")}
|
|
237
248
|
</TabsTrigger>
|
|
238
249
|
<TabsTrigger value="groups">
|
|
239
250
|
<Layers className="h-3 w-3 shrink-0" />
|
|
240
|
-
|
|
251
|
+
{t("results.groups")}
|
|
241
252
|
</TabsTrigger>
|
|
242
253
|
<TabsTrigger value="files">
|
|
243
254
|
<FolderTree className="h-3 w-3 shrink-0" />
|
|
244
|
-
|
|
255
|
+
{t("results.files")}
|
|
245
256
|
</TabsTrigger>
|
|
246
257
|
<TabsTrigger value="stack">
|
|
247
258
|
<GitPullRequestArrow className="h-3 w-3 shrink-0" />
|
|
248
|
-
|
|
259
|
+
{t("results.stack")}
|
|
249
260
|
</TabsTrigger>
|
|
250
261
|
{(!enabledPlugins || enabledPlugins.includes("slides")) && (
|
|
251
262
|
<TabsTrigger value="slides">
|
|
252
263
|
<Presentation className="h-3 w-3 shrink-0" />
|
|
253
|
-
|
|
264
|
+
{t("results.slides")}
|
|
254
265
|
</TabsTrigger>
|
|
255
266
|
)}
|
|
256
267
|
{(!enabledPlugins || enabledPlugins.includes("cartoon")) && (
|
|
257
268
|
<TabsTrigger value="cartoon">
|
|
258
269
|
<Sparkles className="h-3 w-3 shrink-0" />
|
|
259
|
-
|
|
270
|
+
{t("results.comic")}
|
|
260
271
|
</TabsTrigger>
|
|
261
272
|
)}
|
|
262
273
|
</TabsList>
|
|
@@ -3,6 +3,7 @@ import { X, Check, MessageSquare, Loader2, AlertCircle, ExternalLink } from "luc
|
|
|
3
3
|
import { TipTapEditor, getTextWithAnchors } from "./TipTapEditor.tsx";
|
|
4
4
|
import type { useEditor } from "@tiptap/react";
|
|
5
5
|
import { analytics } from "../lib/analytics.ts";
|
|
6
|
+
import { useI18n } from "../lib/i18n/index.ts";
|
|
6
7
|
|
|
7
8
|
type ReviewEvent = "APPROVE" | "REQUEST_CHANGES" | "COMMENT";
|
|
8
9
|
|
|
@@ -36,11 +37,18 @@ interface ReviewModalProps {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
|
|
40
|
+
const { t } = useI18n();
|
|
39
41
|
const [event, setEvent] = useState<ReviewEvent>("APPROVE");
|
|
40
42
|
const [submitting, setSubmitting] = useState(false);
|
|
41
43
|
const [result, setResult] = useState<{ ok: boolean; html_url?: string; error?: string } | null>(null);
|
|
42
44
|
const editorRef = useRef<ReturnType<typeof useEditor>>(null);
|
|
43
45
|
|
|
46
|
+
const eventLabels: Record<ReviewEvent, string> = {
|
|
47
|
+
APPROVE: t("review.approve"),
|
|
48
|
+
REQUEST_CHANGES: t("review.requestChanges"),
|
|
49
|
+
COMMENT: t("review.comment"),
|
|
50
|
+
};
|
|
51
|
+
|
|
44
52
|
const handleSubmit = useCallback(async () => {
|
|
45
53
|
if (submitting) return;
|
|
46
54
|
setSubmitting(true);
|
|
@@ -74,7 +82,7 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
|
|
|
74
82
|
onClick={(e) => e.stopPropagation()}
|
|
75
83
|
>
|
|
76
84
|
<div className="flex items-center justify-between px-4 h-11 border-b">
|
|
77
|
-
<span className="text-sm font-medium">
|
|
85
|
+
<span className="text-sm font-medium">{t("review.submitReview")}</span>
|
|
78
86
|
<button
|
|
79
87
|
type="button"
|
|
80
88
|
onClick={onClose}
|
|
@@ -89,7 +97,7 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
|
|
|
89
97
|
<div className="space-y-4 py-2">
|
|
90
98
|
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
|
91
99
|
<Check className="h-4 w-4" />
|
|
92
|
-
<span className="text-sm font-medium">
|
|
100
|
+
<span className="text-sm font-medium">{t("review.reviewSubmitted")}</span>
|
|
93
101
|
</div>
|
|
94
102
|
{result.html_url && (
|
|
95
103
|
<a
|
|
@@ -98,8 +106,8 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
|
|
|
98
106
|
rel="noopener noreferrer"
|
|
99
107
|
className="inline-flex items-center gap-1.5 text-[11px] text-muted-foreground/60 hover:text-foreground transition-colors"
|
|
100
108
|
>
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
<ExternalLink className="h-3 w-3" />
|
|
110
|
+
{t("review.viewOnGithub")}
|
|
103
111
|
</a>
|
|
104
112
|
)}
|
|
105
113
|
<div className="flex justify-end">
|
|
@@ -108,7 +116,7 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
|
|
|
108
116
|
onClick={onClose}
|
|
109
117
|
className="text-[11px] text-muted-foreground/50 hover:text-foreground px-3 py-1.5 rounded-md hover:bg-accent/40 transition-colors"
|
|
110
118
|
>
|
|
111
|
-
|
|
119
|
+
{t("common.close")}
|
|
112
120
|
</button>
|
|
113
121
|
</div>
|
|
114
122
|
</div>
|
|
@@ -124,19 +132,19 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
|
|
|
124
132
|
event === e.value ? e.activeClass : e.class
|
|
125
133
|
}`}
|
|
126
134
|
>
|
|
127
|
-
{e.
|
|
135
|
+
{eventLabels[e.value]}
|
|
128
136
|
</button>
|
|
129
137
|
))}
|
|
130
138
|
</div>
|
|
131
139
|
|
|
132
140
|
<div>
|
|
133
141
|
<div className="text-[10px] font-medium text-muted-foreground/40 uppercase tracking-wider mb-2">
|
|
134
|
-
|
|
142
|
+
{t("review.message")} {event !== "APPROVE" && <span className="text-red-500/60 normal-case">*</span>}
|
|
135
143
|
</div>
|
|
136
144
|
<div className="rounded-lg border px-3 py-2.5 min-h-[80px] focus-within:border-foreground/15 transition-colors">
|
|
137
145
|
<TipTapEditor
|
|
138
146
|
editorRef={editorRef}
|
|
139
|
-
placeholder={event === "APPROVE" ? "
|
|
147
|
+
placeholder={event === "APPROVE" ? t("review.optionalMessage") : t("review.describeChanges")}
|
|
140
148
|
autoFocus
|
|
141
149
|
submitOnModEnter
|
|
142
150
|
onSubmit={handleSubmit}
|
|
@@ -152,17 +160,17 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
|
|
|
152
160
|
)}
|
|
153
161
|
|
|
154
162
|
<div className="flex items-center justify-between pt-1">
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
<span className="text-[10px] text-muted-foreground/25">
|
|
164
|
+
{t("review.ctrlEnterToSubmit", { key: navigator.platform.includes("Mac") ? "⌘" : "Ctrl" })}
|
|
165
|
+
</span>
|
|
158
166
|
<div className="flex gap-2">
|
|
159
167
|
<button
|
|
160
168
|
type="button"
|
|
161
169
|
onClick={onClose}
|
|
162
170
|
className="text-[11px] text-muted-foreground/50 hover:text-foreground px-3 py-1.5 rounded-md hover:bg-accent/40 transition-colors"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
171
|
+
>
|
|
172
|
+
{t("common.cancel")}
|
|
173
|
+
</button>
|
|
166
174
|
<button
|
|
167
175
|
type="button"
|
|
168
176
|
onClick={handleSubmit}
|
|
@@ -176,7 +184,7 @@ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
|
|
|
176
184
|
) : (
|
|
177
185
|
<MessageSquare className="h-3 w-3" />
|
|
178
186
|
)}
|
|
179
|
-
|
|
187
|
+
{t("common.submit")}
|
|
180
188
|
</button>
|
|
181
189
|
</div>
|
|
182
190
|
</div>
|