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, useCallback, useRef } from "react";
|
|
|
2
2
|
import { Loader2, Presentation, RefreshCw, Download, AlertCircle, ChevronLeft, ChevronRight } from "lucide-react";
|
|
3
3
|
import type { NewprOutput, SlideDeck } from "../../../types/output.ts";
|
|
4
4
|
import { sendNotification } from "../lib/notify.ts";
|
|
5
|
+
import { useI18n } from "../lib/i18n/index.ts";
|
|
5
6
|
|
|
6
7
|
export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?: string | null }) {
|
|
7
8
|
const [state, setState] = useState<"idle" | "loading" | "done" | "error">("idle");
|
|
@@ -11,6 +12,7 @@ export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?
|
|
|
11
12
|
const [progressDetail, setProgressDetail] = useState<{ current: number; total: number } | null>(null);
|
|
12
13
|
const [currentSlide, setCurrentSlide] = useState(0);
|
|
13
14
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
15
|
+
const { t } = useI18n();
|
|
14
16
|
|
|
15
17
|
const stopPolling = useCallback(() => {
|
|
16
18
|
if (pollRef.current) {
|
|
@@ -53,11 +55,11 @@ export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?
|
|
|
53
55
|
if (partial?.slides?.length) {
|
|
54
56
|
setDeck(partial);
|
|
55
57
|
setState("done");
|
|
56
|
-
sendNotification("
|
|
58
|
+
sendNotification(t("slides.slidesReady"), t("slides.nSlidesGenerated", { n: partial.slides.length }));
|
|
57
59
|
}
|
|
58
60
|
} else if (job.status === "error") {
|
|
59
61
|
stopPolling();
|
|
60
|
-
setError(job.message ?? "
|
|
62
|
+
setError(job.message ?? t("slides.failed"));
|
|
61
63
|
if (partial?.slides?.length) {
|
|
62
64
|
setDeck(partial);
|
|
63
65
|
setState("done");
|
|
@@ -67,13 +69,13 @@ export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?
|
|
|
67
69
|
}
|
|
68
70
|
} catch {}
|
|
69
71
|
}, 1000);
|
|
70
|
-
}, [sessionId, stopPolling]);
|
|
72
|
+
}, [sessionId, stopPolling, t]);
|
|
71
73
|
|
|
72
74
|
const generate = useCallback(async (resume = false) => {
|
|
73
75
|
if (!sessionId) return;
|
|
74
76
|
setState("loading");
|
|
75
77
|
setError(null);
|
|
76
|
-
setProgress(resume ? "
|
|
78
|
+
setProgress(resume ? t("slides.resuming") : t("slides.starting"));
|
|
77
79
|
setProgressDetail(null);
|
|
78
80
|
|
|
79
81
|
try {
|
|
@@ -91,7 +93,7 @@ export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?
|
|
|
91
93
|
setError(err instanceof Error ? err.message : String(err));
|
|
92
94
|
setState("error");
|
|
93
95
|
}
|
|
94
|
-
}, [sessionId, startPolling]);
|
|
96
|
+
}, [sessionId, startPolling, t]);
|
|
95
97
|
|
|
96
98
|
useEffect(() => {
|
|
97
99
|
if (!sessionId || state !== "idle") return;
|
|
@@ -122,9 +124,9 @@ export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?
|
|
|
122
124
|
<div className="pt-8 flex flex-col items-center">
|
|
123
125
|
<div className="w-full max-w-sm space-y-6">
|
|
124
126
|
<div className="space-y-2">
|
|
125
|
-
<h3 className="text-xs font-medium">
|
|
127
|
+
<h3 className="text-xs font-medium">{t("slides.title")}</h3>
|
|
126
128
|
<p className="text-[11px] text-muted-foreground/60 leading-relaxed">
|
|
127
|
-
|
|
129
|
+
{t("slides.description")}
|
|
128
130
|
</p>
|
|
129
131
|
</div>
|
|
130
132
|
<button
|
|
@@ -133,7 +135,7 @@ export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?
|
|
|
133
135
|
className="w-full flex items-center justify-center gap-2 h-9 rounded-lg bg-foreground text-background text-xs font-medium hover:opacity-90 transition-opacity"
|
|
134
136
|
>
|
|
135
137
|
<Presentation className="h-3.5 w-3.5" />
|
|
136
|
-
|
|
138
|
+
{t("slides.generateSlides")}
|
|
137
139
|
</button>
|
|
138
140
|
</div>
|
|
139
141
|
</div>
|
|
@@ -183,7 +185,7 @@ export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?
|
|
|
183
185
|
</div>
|
|
184
186
|
)}
|
|
185
187
|
{partialSlides.length === 0 && !progressDetail && (
|
|
186
|
-
<p className="text-[10px] text-muted-foreground/30 text-center">
|
|
188
|
+
<p className="text-[10px] text-muted-foreground/30 text-center">{t("slides.takesMinutes")}</p>
|
|
187
189
|
)}
|
|
188
190
|
</div>
|
|
189
191
|
);
|
|
@@ -196,7 +198,7 @@ export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?
|
|
|
196
198
|
<div className="rounded-lg border border-destructive/20 bg-destructive/5 px-4 py-3 flex items-start gap-2.5">
|
|
197
199
|
<AlertCircle className="h-3.5 w-3.5 text-destructive shrink-0 mt-0.5" />
|
|
198
200
|
<div className="space-y-1 min-w-0">
|
|
199
|
-
<p className="text-xs text-destructive font-medium">
|
|
201
|
+
<p className="text-xs text-destructive font-medium">{t("slides.failed")}</p>
|
|
200
202
|
<p className="text-[11px] text-destructive/70 break-words">{error}</p>
|
|
201
203
|
</div>
|
|
202
204
|
</div>
|
|
@@ -206,7 +208,7 @@ export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?
|
|
|
206
208
|
className="w-full flex items-center justify-center gap-2 h-9 rounded-lg border text-xs text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors"
|
|
207
209
|
>
|
|
208
210
|
<RefreshCw className="h-3 w-3" />
|
|
209
|
-
|
|
211
|
+
{t("common.tryAgain")}
|
|
210
212
|
</button>
|
|
211
213
|
</div>
|
|
212
214
|
</div>
|
|
@@ -225,7 +227,7 @@ export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?
|
|
|
225
227
|
<div className="flex items-center gap-2 px-3 py-2 rounded-lg border border-yellow-500/20 bg-yellow-500/5">
|
|
226
228
|
<AlertCircle className="h-3.5 w-3.5 text-yellow-600 dark:text-yellow-400 shrink-0" />
|
|
227
229
|
<span className="text-[11px] text-yellow-700 dark:text-yellow-300 flex-1">
|
|
228
|
-
{
|
|
230
|
+
{t("slides.slidesFailed", { n: deck.failedIndices!.length })}
|
|
229
231
|
</span>
|
|
230
232
|
<button
|
|
231
233
|
type="button"
|
|
@@ -233,7 +235,7 @@ export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?
|
|
|
233
235
|
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"
|
|
234
236
|
>
|
|
235
237
|
<RefreshCw className="h-3 w-3" />
|
|
236
|
-
|
|
238
|
+
{t("slides.retryFailed")}
|
|
237
239
|
</button>
|
|
238
240
|
</div>
|
|
239
241
|
)}
|
|
@@ -280,7 +282,7 @@ export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?
|
|
|
280
282
|
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-[11px] text-muted-foreground/60 hover:text-foreground hover:bg-accent/50 transition-colors"
|
|
281
283
|
>
|
|
282
284
|
<Download className="h-3 w-3" />
|
|
283
|
-
|
|
285
|
+
{t("common.download")}
|
|
284
286
|
</button>
|
|
285
287
|
<button
|
|
286
288
|
type="button"
|
|
@@ -288,7 +290,7 @@ export function SlidesPanel({ data, sessionId }: { data: NewprOutput; sessionId?
|
|
|
288
290
|
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-[11px] text-muted-foreground/60 hover:text-foreground hover:bg-accent/50 transition-colors"
|
|
289
291
|
>
|
|
290
292
|
<RefreshCw className="h-3 w-3" />
|
|
291
|
-
|
|
293
|
+
{t("common.regenerate")}
|
|
292
294
|
</button>
|
|
293
295
|
</div>
|
|
294
296
|
</div>
|
|
@@ -4,15 +4,16 @@ import { useStack } from "../hooks/useStack.ts";
|
|
|
4
4
|
import { FeasibilityAlert } from "../components/FeasibilityAlert.tsx";
|
|
5
5
|
import { StackDagView } from "../components/StackDagView.tsx";
|
|
6
6
|
import { StackWarnings } from "../components/StackWarnings.tsx";
|
|
7
|
+
import { useI18n, type TranslationKey } from "../lib/i18n/index.ts";
|
|
7
8
|
|
|
8
9
|
type StackPhase = "idle" | "partitioning" | "planning" | "executing" | "publishing" | "done" | "error";
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
{ phase: "partitioning"
|
|
12
|
-
{ phase: "planning"
|
|
13
|
-
{ phase: "executing"
|
|
14
|
-
{ phase: "publishing"
|
|
15
|
-
]
|
|
11
|
+
const PIPELINE_STEP_KEYS: Array<{ phase: string; labelKey: TranslationKey; descKey: TranslationKey }> = [
|
|
12
|
+
{ phase: "partitioning", labelKey: "stack.partition", descKey: "stack.partitionDesc" },
|
|
13
|
+
{ phase: "planning", labelKey: "stack.plan", descKey: "stack.planDesc" },
|
|
14
|
+
{ phase: "executing", labelKey: "stack.execute", descKey: "stack.executeDesc" },
|
|
15
|
+
{ phase: "publishing", labelKey: "stack.publish", descKey: "stack.publishDesc" },
|
|
16
|
+
];
|
|
16
17
|
|
|
17
18
|
function getStepState(stepPhase: string, currentPhase: StackPhase, isDone: boolean) {
|
|
18
19
|
const order = ["partitioning", "planning", "executing", "publishing"];
|
|
@@ -26,12 +27,13 @@ function getStepState(stepPhase: string, currentPhase: StackPhase, isDone: boole
|
|
|
26
27
|
|
|
27
28
|
function PipelineTimeline({ phase }: { phase: StackPhase }) {
|
|
28
29
|
const isDone = phase === "done";
|
|
30
|
+
const { t } = useI18n();
|
|
29
31
|
|
|
30
32
|
return (
|
|
31
33
|
<div className="relative flex flex-col gap-0 py-1">
|
|
32
|
-
{
|
|
34
|
+
{PIPELINE_STEP_KEYS.map((step, i) => {
|
|
33
35
|
const state = getStepState(step.phase, phase, isDone);
|
|
34
|
-
const isLast = i ===
|
|
36
|
+
const isLast = i === PIPELINE_STEP_KEYS.length - 1;
|
|
35
37
|
|
|
36
38
|
return (
|
|
37
39
|
<div key={step.phase} className="relative flex items-start gap-3">
|
|
@@ -57,10 +59,10 @@ function PipelineTimeline({ phase }: { phase: StackPhase }) {
|
|
|
57
59
|
: state === "active" ? "text-foreground"
|
|
58
60
|
: "text-muted-foreground/40"
|
|
59
61
|
}`}>
|
|
60
|
-
{step.
|
|
62
|
+
{t(step.labelKey)}
|
|
61
63
|
</span>
|
|
62
64
|
{state === "active" && (
|
|
63
|
-
<p className="text-[11px] text-muted-foreground/50 mt-0.5">{step.
|
|
65
|
+
<p className="text-[11px] text-muted-foreground/50 mt-0.5">{t(step.descKey)}</p>
|
|
64
66
|
)}
|
|
65
67
|
</div>
|
|
66
68
|
</div>
|
|
@@ -75,6 +77,7 @@ function EnvVarsInput({ envVars, setEnvVars }: { envVars: Record<string, string>
|
|
|
75
77
|
const entries = Object.entries(envVars);
|
|
76
78
|
return entries.length > 0 ? entries.map(([key, value]) => ({ key, value })) : [];
|
|
77
79
|
});
|
|
80
|
+
const { t } = useI18n();
|
|
78
81
|
|
|
79
82
|
useEffect(() => {
|
|
80
83
|
const record: Record<string, string> = {};
|
|
@@ -97,14 +100,14 @@ function EnvVarsInput({ envVars, setEnvVars }: { envVars: Record<string, string>
|
|
|
97
100
|
<details className="mb-5 group">
|
|
98
101
|
<summary className="cursor-pointer list-none flex items-center gap-2 text-[11px] text-muted-foreground/40 hover:text-muted-foreground/60 transition-colors select-none">
|
|
99
102
|
<KeyRound className="h-3 w-3" />
|
|
100
|
-
<span>
|
|
103
|
+
<span>{t("stack.envVars")}</span>
|
|
101
104
|
{rows.length > 0 && (
|
|
102
105
|
<span className="text-[10px] tabular-nums text-muted-foreground/25">({rows.length})</span>
|
|
103
106
|
)}
|
|
104
107
|
</summary>
|
|
105
108
|
<div className="mt-2.5 space-y-2">
|
|
106
109
|
<p className="text-[10px] text-muted-foreground/30 leading-relaxed">
|
|
107
|
-
|
|
110
|
+
{t("stack.envVarsDesc")}
|
|
108
111
|
</p>
|
|
109
112
|
{rows.map((row, idx) => (
|
|
110
113
|
<div key={idx} className="flex items-center gap-1.5">
|
|
@@ -137,7 +140,7 @@ function EnvVarsInput({ envVars, setEnvVars }: { envVars: Record<string, string>
|
|
|
137
140
|
className="flex items-center gap-1.5 text-[10px] text-muted-foreground/30 hover:text-muted-foreground/60 transition-colors"
|
|
138
141
|
>
|
|
139
142
|
<Plus className="h-3 w-3" />
|
|
140
|
-
|
|
143
|
+
{t("stack.addVariable")}
|
|
141
144
|
</button>
|
|
142
145
|
</div>
|
|
143
146
|
</details>
|
|
@@ -156,12 +159,14 @@ interface QualityGateResultData {
|
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
function QualityGateResults({ result }: { result: QualityGateResultData }) {
|
|
162
|
+
const { t } = useI18n();
|
|
163
|
+
|
|
159
164
|
if (!result.ran && result.skippedReason) {
|
|
160
165
|
return (
|
|
161
166
|
<div className="flex items-center gap-2.5 rounded-lg bg-foreground/[0.03] px-3.5 py-2.5">
|
|
162
167
|
<Circle className="h-3.5 w-3.5 text-muted-foreground/30 shrink-0" />
|
|
163
168
|
<span className="text-[11px] text-muted-foreground/40">
|
|
164
|
-
|
|
169
|
+
{t("stack.qualityGateSkipped", { reason: result.skippedReason })}
|
|
165
170
|
</span>
|
|
166
171
|
</div>
|
|
167
172
|
);
|
|
@@ -188,9 +193,9 @@ function QualityGateResults({ result }: { result: QualityGateResultData }) {
|
|
|
188
193
|
? "text-green-700/70 dark:text-green-300/70"
|
|
189
194
|
: "text-yellow-700/70 dark:text-yellow-300/70"
|
|
190
195
|
}`}>
|
|
191
|
-
|
|
192
|
-
?
|
|
193
|
-
:
|
|
196
|
+
{t("stack.qualityGate")}: {allPassed
|
|
197
|
+
? t("stack.qualityGateAllPassed", { n: totalGroups })
|
|
198
|
+
: t("stack.qualityGatePartial", { passed: passedGroups, total: totalGroups })
|
|
194
199
|
}
|
|
195
200
|
</span>
|
|
196
201
|
</div>
|
|
@@ -198,7 +203,7 @@ function QualityGateResults({ result }: { result: QualityGateResultData }) {
|
|
|
198
203
|
{failedGroups.length > 0 && (
|
|
199
204
|
<details className="rounded-lg border border-yellow-500/20 bg-yellow-500/[0.03]">
|
|
200
205
|
<summary className="cursor-pointer list-none px-3.5 py-2.5 text-[11px] text-yellow-700/70 dark:text-yellow-300/70 hover:bg-yellow-500/[0.04] transition-colors select-none">
|
|
201
|
-
{
|
|
206
|
+
{t("stack.groupsWithWarnings", { n: failedGroups.length })}
|
|
202
207
|
</summary>
|
|
203
208
|
<div className="px-3.5 pb-3 space-y-2.5">
|
|
204
209
|
{failedGroups.map((group) => (
|
|
@@ -233,6 +238,7 @@ interface StackPanelProps {
|
|
|
233
238
|
|
|
234
239
|
export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
235
240
|
const stack = useStack(sessionId, { onTrackAnalysis });
|
|
241
|
+
const { t } = useI18n();
|
|
236
242
|
const publishedCount = stack.publishResult?.prs.length ?? 0;
|
|
237
243
|
const pushedCount = stack.publishResult?.branches.filter((b) => b.pushed).length ?? 0;
|
|
238
244
|
const publishFailures = stack.publishResult
|
|
@@ -255,17 +261,17 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
255
261
|
<Layers className="h-4 w-4 text-foreground/50" />
|
|
256
262
|
</div>
|
|
257
263
|
<div>
|
|
258
|
-
<h3 className="text-[13px] font-semibold text-foreground">
|
|
259
|
-
<p className="text-[11px] text-muted-foreground/50">
|
|
264
|
+
<h3 className="text-[13px] font-semibold text-foreground">{t("stack.title")}</h3>
|
|
265
|
+
<p className="text-[11px] text-muted-foreground/50">{t("stack.subtitle")}</p>
|
|
260
266
|
</div>
|
|
261
267
|
</div>
|
|
262
268
|
|
|
263
269
|
<p className="text-[11px] text-muted-foreground/40 leading-[1.6] mb-5">
|
|
264
|
-
|
|
270
|
+
{t("stack.description")}
|
|
265
271
|
</p>
|
|
266
272
|
|
|
267
273
|
<div className="flex items-center gap-3 mb-5">
|
|
268
|
-
<span className="text-[11px] text-muted-foreground/50">
|
|
274
|
+
<span className="text-[11px] text-muted-foreground/50">{t("stack.maxPrs")}</span>
|
|
269
275
|
<div className="flex items-center">
|
|
270
276
|
<button
|
|
271
277
|
type="button"
|
|
@@ -300,7 +306,7 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
300
306
|
className="w-full flex items-center justify-center gap-2 rounded-lg bg-foreground text-background px-4 py-2.5 text-[12px] font-medium hover:bg-foreground/90 transition-colors"
|
|
301
307
|
>
|
|
302
308
|
<Play className="h-3.5 w-3.5" />
|
|
303
|
-
|
|
309
|
+
{t("stack.startStacking")}
|
|
304
310
|
</button>
|
|
305
311
|
</div>
|
|
306
312
|
);
|
|
@@ -313,12 +319,12 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
313
319
|
<div className="flex items-center gap-2">
|
|
314
320
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
315
321
|
<Layers className="h-3.5 w-3.5 text-muted-foreground/40 shrink-0" />
|
|
316
|
-
<span className="text-[12px] font-semibold text-foreground">
|
|
322
|
+
<span className="text-[12px] font-semibold text-foreground">{t("stack.title")}</span>
|
|
317
323
|
{stack.phase === "done" && !stack.publishResult && (
|
|
318
|
-
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-foreground/[0.06] text-muted-foreground/60">
|
|
324
|
+
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-foreground/[0.06] text-muted-foreground/60">{t("stack.ready")}</span>
|
|
319
325
|
)}
|
|
320
326
|
{stack.publishResult && (
|
|
321
|
-
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-green-500/10 text-green-600 dark:text-green-400">
|
|
327
|
+
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-green-500/10 text-green-600 dark:text-green-400">{t("stack.published")}</span>
|
|
322
328
|
)}
|
|
323
329
|
</div>
|
|
324
330
|
{stack.phase === "done" && (
|
|
@@ -338,7 +344,7 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
338
344
|
{stack.phase === "publishing" && (
|
|
339
345
|
<div className="rounded-lg bg-blue-500/[0.05] px-3.5 py-2.5">
|
|
340
346
|
<p className="text-[10px] text-blue-700/80 dark:text-blue-300/80 leading-relaxed">
|
|
341
|
-
|
|
347
|
+
{t("stack.publishingInfo")}
|
|
342
348
|
</p>
|
|
343
349
|
</div>
|
|
344
350
|
)}
|
|
@@ -362,7 +368,7 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
362
368
|
className="flex items-center gap-1.5 text-[11px] text-muted-foreground/40 hover:text-foreground transition-colors"
|
|
363
369
|
>
|
|
364
370
|
<RotateCcw className="h-3 w-3" />
|
|
365
|
-
|
|
371
|
+
{t("common.tryAgain")}
|
|
366
372
|
</button>
|
|
367
373
|
</div>
|
|
368
374
|
)}
|
|
@@ -379,11 +385,11 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
379
385
|
<div className="space-y-1">
|
|
380
386
|
<div className="flex items-center justify-between mb-2">
|
|
381
387
|
<span className="text-[11px] font-medium text-muted-foreground/40">
|
|
382
|
-
{stack.plan.groups.length}
|
|
388
|
+
{t("stack.nPrs", { n: stack.plan.groups.length })}
|
|
383
389
|
</span>
|
|
384
390
|
{stack.plan.groups.length > 0 && (
|
|
385
391
|
<span className="text-[10px] text-muted-foreground/25 tabular-nums">
|
|
386
|
-
{stack.plan.groups.reduce((sum, g) => sum + g.files.length, 0)}
|
|
392
|
+
{t("stack.nFilesTotal", { n: stack.plan.groups.reduce((sum, g) => sum + g.files.length, 0) })}
|
|
387
393
|
</span>
|
|
388
394
|
)}
|
|
389
395
|
</div>
|
|
@@ -412,8 +418,8 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
412
418
|
: "text-red-600/80 dark:text-red-400/80"
|
|
413
419
|
}`}>
|
|
414
420
|
{stack.verifyResult.verified
|
|
415
|
-
? "
|
|
416
|
-
:
|
|
421
|
+
? t("stack.treeEquivalenceVerified")
|
|
422
|
+
: t("stack.verificationFailed", { errors: stack.verifyResult.errors.join(", ") })
|
|
417
423
|
}
|
|
418
424
|
</span>
|
|
419
425
|
</div>
|
|
@@ -432,7 +438,7 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
432
438
|
<div className="flex items-center justify-between px-1">
|
|
433
439
|
<div className="flex items-center gap-2">
|
|
434
440
|
<FileText className="h-3.5 w-3.5 text-muted-foreground/60" />
|
|
435
|
-
<span className="text-[11px] font-medium text-foreground/80">
|
|
441
|
+
<span className="text-[11px] font-medium text-foreground/80">{t("stack.descriptionPreview")}</span>
|
|
436
442
|
</div>
|
|
437
443
|
<button
|
|
438
444
|
type="button"
|
|
@@ -440,7 +446,7 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
440
446
|
className="inline-flex items-center gap-1 text-[10px] text-muted-foreground/45 hover:text-foreground/70 transition-colors"
|
|
441
447
|
>
|
|
442
448
|
<RefreshCw className="h-3 w-3" />
|
|
443
|
-
|
|
449
|
+
{t("common.refresh")}
|
|
444
450
|
</button>
|
|
445
451
|
</div>
|
|
446
452
|
|
|
@@ -451,7 +457,7 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
451
457
|
{stack.publishPreviewLoading && (
|
|
452
458
|
<div className="flex items-center gap-2 px-2.5 py-2 text-[10px] text-muted-foreground/45">
|
|
453
459
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
454
|
-
|
|
460
|
+
{t("stack.preparingPreview")}
|
|
455
461
|
</div>
|
|
456
462
|
)}
|
|
457
463
|
|
|
@@ -495,7 +501,7 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
495
501
|
className="w-full flex items-center justify-center gap-2 rounded-lg bg-foreground text-background px-4 py-2.5 text-[12px] font-medium hover:bg-foreground/90 transition-colors"
|
|
496
502
|
>
|
|
497
503
|
<Upload className="h-3.5 w-3.5" />
|
|
498
|
-
|
|
504
|
+
{t("stack.publishAsDraft")}
|
|
499
505
|
</button>
|
|
500
506
|
)}
|
|
501
507
|
|
|
@@ -504,35 +510,35 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
504
510
|
<div className="flex items-center justify-between px-1">
|
|
505
511
|
<div className="flex items-center gap-2">
|
|
506
512
|
<GitPullRequestArrow className="h-3.5 w-3.5 text-green-600/70 dark:text-green-400/70" />
|
|
507
|
-
<span className="text-[11px] font-medium text-foreground/80">
|
|
513
|
+
<span className="text-[11px] font-medium text-foreground/80">{t("stack.draftPublishResults")}</span>
|
|
508
514
|
</div>
|
|
509
|
-
<span className="text-[10px] text-muted-foreground/35 tabular-nums">{publishedCount
|
|
515
|
+
<span className="text-[10px] text-muted-foreground/35 tabular-nums">{t("stack.nOfNCreated", { created: publishedCount, total: pushedCount })}</span>
|
|
510
516
|
</div>
|
|
511
517
|
|
|
512
518
|
<div className="flex flex-wrap items-center gap-2 px-1">
|
|
513
519
|
<button
|
|
514
520
|
type="button"
|
|
515
521
|
onClick={() => {
|
|
516
|
-
if (!confirm("
|
|
522
|
+
if (!confirm(t("stack.confirmCloseAll"))) return;
|
|
517
523
|
stack.cleanupPublished("close");
|
|
518
524
|
}}
|
|
519
525
|
disabled={stack.publishCleanupLoading}
|
|
520
526
|
className="inline-flex items-center gap-1.5 h-7 px-2.5 rounded-md border border-border text-[10px] text-foreground/70 hover:bg-accent/30 transition-colors disabled:opacity-50"
|
|
521
527
|
>
|
|
522
528
|
{stack.publishCleanupLoading ? <Loader2 className="h-3 w-3 animate-spin" /> : <XCircle className="h-3 w-3" />}
|
|
523
|
-
|
|
529
|
+
{t("stack.closeAll")}
|
|
524
530
|
</button>
|
|
525
531
|
<button
|
|
526
532
|
type="button"
|
|
527
533
|
onClick={() => {
|
|
528
|
-
if (!confirm("
|
|
534
|
+
if (!confirm(t("stack.confirmCloseDelete"))) return;
|
|
529
535
|
stack.cleanupPublished("delete");
|
|
530
536
|
}}
|
|
531
537
|
disabled={stack.publishCleanupLoading}
|
|
532
538
|
className="inline-flex items-center gap-1.5 h-7 px-2.5 rounded-md border border-red-500/25 text-[10px] text-red-600/80 dark:text-red-300/80 hover:bg-red-500/10 transition-colors disabled:opacity-50"
|
|
533
539
|
>
|
|
534
540
|
{stack.publishCleanupLoading ? <Loader2 className="h-3 w-3 animate-spin" /> : <Trash2 className="h-3 w-3" />}
|
|
535
|
-
|
|
541
|
+
{t("stack.closeDeleteBranches")}
|
|
536
542
|
</button>
|
|
537
543
|
</div>
|
|
538
544
|
|
|
@@ -582,13 +588,13 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
582
588
|
))}
|
|
583
589
|
</div>
|
|
584
590
|
) : (
|
|
585
|
-
<p className="text-[10px] text-muted-foreground/35 px-2.5 py-2">
|
|
591
|
+
<p className="text-[10px] text-muted-foreground/35 px-2.5 py-2">{t("stack.noDraftPrUrls")}</p>
|
|
586
592
|
)}
|
|
587
593
|
|
|
588
594
|
{publishFailures.length > 0 && (
|
|
589
595
|
<div className="rounded-md bg-yellow-500/[0.06] px-2.5 py-2 space-y-1">
|
|
590
596
|
<p className="text-[10px] text-yellow-700/80 dark:text-yellow-300/80">
|
|
591
|
-
|
|
597
|
+
{t("stack.branchesPushedNotCreated")}
|
|
592
598
|
</p>
|
|
593
599
|
{publishFailures.map((branch) => (
|
|
594
600
|
<div key={branch.name} className="text-[10px] font-mono text-yellow-700/70 dark:text-yellow-300/70 truncate">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { NewprOutput } from "../../../types/output.ts";
|
|
2
2
|
import { Markdown } from "../components/Markdown.tsx";
|
|
3
3
|
import { ChatMessages } from "../components/ChatSection.tsx";
|
|
4
|
+
import { useI18n } from "../lib/i18n/index.ts";
|
|
4
5
|
|
|
5
6
|
const TYPE_DOT: Record<string, string> = {
|
|
6
7
|
feature: "bg-blue-500",
|
|
@@ -22,6 +23,7 @@ export function StoryPanel({
|
|
|
22
23
|
onAnchorClick: (kind: "group" | "file" | "line", id: string) => void;
|
|
23
24
|
}) {
|
|
24
25
|
const { summary, groups, narrative } = data;
|
|
26
|
+
const { t } = useI18n();
|
|
25
27
|
|
|
26
28
|
return (
|
|
27
29
|
<div className="pt-5 space-y-6">
|
|
@@ -30,11 +32,11 @@ export function StoryPanel({
|
|
|
30
32
|
|
|
31
33
|
<div className="grid grid-cols-2 gap-x-6 gap-y-3">
|
|
32
34
|
<div>
|
|
33
|
-
<div className="text-xs font-medium text-muted-foreground/40 uppercase tracking-wider mb-1">
|
|
35
|
+
<div className="text-xs font-medium text-muted-foreground/40 uppercase tracking-wider mb-1">{t("story.scope")}</div>
|
|
34
36
|
<p className="text-sm text-muted-foreground/70 leading-relaxed">{summary.scope}</p>
|
|
35
37
|
</div>
|
|
36
38
|
<div>
|
|
37
|
-
<div className="text-xs font-medium text-muted-foreground/40 uppercase tracking-wider mb-1">
|
|
39
|
+
<div className="text-xs font-medium text-muted-foreground/40 uppercase tracking-wider mb-1">{t("story.impact")}</div>
|
|
38
40
|
<p className="text-sm text-muted-foreground/70 leading-relaxed">{summary.impact}</p>
|
|
39
41
|
</div>
|
|
40
42
|
</div>
|
|
@@ -62,7 +64,7 @@ export function StoryPanel({
|
|
|
62
64
|
</div>
|
|
63
65
|
|
|
64
66
|
<div className="border-t pt-5">
|
|
65
|
-
<div className="text-xs font-medium text-muted-foreground/40 uppercase tracking-wider mb-4">
|
|
67
|
+
<div className="text-xs font-medium text-muted-foreground/40 uppercase tracking-wider mb-4">{t("story.walkthrough")}</div>
|
|
66
68
|
<Markdown onAnchorClick={onAnchorClick} activeId={activeId}>
|
|
67
69
|
{narrative}
|
|
68
70
|
</Markdown>
|
package/src/web/server/routes.ts
CHANGED
|
@@ -669,27 +669,28 @@ Before posting an inline comment, ALWAYS call \`get_file_diff\` first to find th
|
|
|
669
669
|
const enabledPlugins = stored.enabled_plugins ?? pluginList.map((p) => p.id);
|
|
670
670
|
const agents = await detectAgents();
|
|
671
671
|
const telemetryConsent = await getTelemetryConsent();
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
672
|
+
return json({
|
|
673
|
+
model: config.model,
|
|
674
|
+
agent: config.agent ?? null,
|
|
675
|
+
language: config.language,
|
|
676
|
+
max_files: config.max_files,
|
|
677
|
+
timeout: config.timeout,
|
|
678
|
+
concurrency: config.concurrency,
|
|
679
|
+
has_api_key: !!config.openrouter_api_key,
|
|
680
|
+
has_agent_fallback: agents.length > 0,
|
|
681
|
+
has_github_token: !!token,
|
|
682
|
+
enabled_plugins: enabledPlugins,
|
|
683
|
+
available_plugins: pluginList,
|
|
684
|
+
telemetry_consent: telemetryConsent,
|
|
685
|
+
custom_prompt: config.custom_prompt ?? "",
|
|
686
|
+
defaults: {
|
|
687
|
+
model: DEFAULT_CONFIG.model,
|
|
688
|
+
language: DEFAULT_CONFIG.language,
|
|
689
|
+
max_files: DEFAULT_CONFIG.max_files,
|
|
690
|
+
timeout: DEFAULT_CONFIG.timeout,
|
|
691
|
+
concurrency: DEFAULT_CONFIG.concurrency,
|
|
692
|
+
},
|
|
693
|
+
});
|
|
693
694
|
},
|
|
694
695
|
|
|
695
696
|
"PUT /api/config": async (req: Request) => {
|
|
@@ -732,6 +733,11 @@ Before posting an inline comment, ALWAYS call \`get_file_diff\` first to find th
|
|
|
732
733
|
if ((body as Record<string, unknown>).enabled_plugins !== undefined) {
|
|
733
734
|
update.enabled_plugins = (body as Record<string, unknown>).enabled_plugins as string[];
|
|
734
735
|
}
|
|
736
|
+
if ((body as Record<string, unknown>).custom_prompt !== undefined) {
|
|
737
|
+
const val = (body as Record<string, unknown>).custom_prompt as string;
|
|
738
|
+
update.custom_prompt = val || undefined;
|
|
739
|
+
config.custom_prompt = val || undefined;
|
|
740
|
+
}
|
|
735
741
|
|
|
736
742
|
const telemetryConsentVal = (body as Record<string, unknown>).telemetry_consent as string | undefined;
|
|
737
743
|
if (telemetryConsentVal === "granted" || telemetryConsentVal === "denied") {
|