create-principles-disciple 1.109.0 → 1.111.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/console/dist/server/config/pd-config-store.d.ts +29 -0
- package/console/dist/server/config/pd-config-store.js +55 -0
- package/console/dist/server/feedback/channels.d.ts +36 -0
- package/console/dist/server/feedback/channels.js +89 -0
- package/console/dist/server/feedback/github-adapter.d.ts +44 -0
- package/console/dist/server/feedback/github-adapter.js +125 -0
- package/console/dist/server/feedback/ingest-adapter.d.ts +45 -0
- package/console/dist/server/feedback/ingest-adapter.js +129 -0
- package/console/dist/server/feedback/submit-service.d.ts +44 -0
- package/console/dist/server/feedback/submit-service.js +177 -0
- package/console/dist/server/index.js +23 -7
- package/console/dist/server/models/FeedbackReportConsoleModel.d.ts +8 -0
- package/console/dist/server/models/FeedbackReportConsoleModel.js +45 -0
- package/console/dist/server/routes/feedback-reports.d.ts +24 -0
- package/console/dist/server/routes/feedback-reports.js +151 -0
- package/console/dist/ui/api.d.ts +11 -3
- package/console/dist/ui/api.js +25 -2
- package/console/dist/ui/i18n/en.json +48 -2
- package/console/dist/ui/i18n/zh-CN.json +48 -2
- package/console/dist/ui/pages/ReportProblemValidators.d.ts +30 -0
- package/console/dist/ui/pages/ReportProblemValidators.js +66 -0
- package/console/dist/ui/pages/activation/ActivationPage.js +14 -3
- package/console/dist/ui/pages/pain/PainPage.js +6 -1
- package/console/dist/ui/pages/principles/PrincipleDetailPage.js +4 -1
- package/console/dist/ui/pages/report-problem/ReportProblemPage.js +263 -70
- package/console/dist/ui/utils/validators.d.ts +22 -0
- package/console/dist/ui/utils/validators.js +66 -0
- package/console/dist/web/assets/app.css +42 -0
- package/console/dist/web/assets/app.js +699 -127
- package/console/package.json +4 -4
- package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +4 -0
- package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
- package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.d.ts +2 -0
- package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.js +126 -0
- package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.js.map +1 -0
- package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.d.ts +2 -0
- package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.js +278 -0
- package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.js.map +1 -0
- package/core/dist/runtime-v2/feedback/create-report.d.ts.map +1 -1
- package/core/dist/runtime-v2/feedback/create-report.js +38 -1
- package/core/dist/runtime-v2/feedback/create-report.js.map +1 -1
- package/core/dist/runtime-v2/feedback/feedback-types.d.ts +60 -0
- package/core/dist/runtime-v2/feedback/feedback-types.d.ts.map +1 -1
- package/core/dist/runtime-v2/feedback/feedback-types.js +58 -0
- package/core/dist/runtime-v2/feedback/feedback-types.js.map +1 -1
- package/core/dist/runtime-v2/feedback/fingerprint.d.ts +31 -0
- package/core/dist/runtime-v2/feedback/fingerprint.d.ts.map +1 -0
- package/core/dist/runtime-v2/feedback/fingerprint.js +51 -0
- package/core/dist/runtime-v2/feedback/fingerprint.js.map +1 -0
- package/core/dist/runtime-v2/feedback/index.d.ts +3 -2
- package/core/dist/runtime-v2/feedback/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/feedback/index.js +2 -1
- package/core/dist/runtime-v2/feedback/index.js.map +1 -1
- package/core/dist/runtime-v2/feedback/privacy-preview.d.ts.map +1 -1
- package/core/dist/runtime-v2/feedback/privacy-preview.js +43 -0
- package/core/dist/runtime-v2/feedback/privacy-preview.js.map +1 -1
- package/core/dist/runtime-v2/feedback/render-markdown.d.ts.map +1 -1
- package/core/dist/runtime-v2/feedback/render-markdown.js +33 -0
- package/core/dist/runtime-v2/feedback/render-markdown.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +2 -2
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +1 -1
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js.map +1 -1
- package/dist/mvp-config.d.ts +22 -1
- package/dist/mvp-config.d.ts.map +1 -1
- package/dist/mvp-config.js +36 -1
- package/dist/mvp-config.js.map +1 -1
- package/package.json +1 -1
- package/plugin/dist/bundle.js +302 -302
- package/plugin/dist/openclaw.plugin.json +1 -18
- package/plugin/openclaw.plugin.json +2 -19
- package/plugin/package.json +3 -3
- package/plugin/scripts/sync-plugin.mjs +34 -33
|
@@ -6,23 +6,62 @@ import { toast } from "sonner";
|
|
|
6
6
|
import { PageShell } from "../../components/layout/page-shell.js";
|
|
7
7
|
import { PageLoading } from "../../components/layout/page-loading.js";
|
|
8
8
|
import { SectionTitle } from "../../components/layout/section-title.js";
|
|
9
|
-
import {
|
|
9
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "../../components/ui/dialog.js";
|
|
10
|
+
import { createFeedbackReport, listFeedbackReports, getFeedbackReport, deleteFeedbackReport, fetchConfigSummary, fetchFeedbackChannels, submitFeedbackReport, markFeedbackReportSent, request, } from "../../api.js";
|
|
10
11
|
import { enumLabel } from "../../utils/enum-labels.js";
|
|
11
|
-
import { parseDraftRecord, parseDraftSummary, parseEnvelopeReport, getErrorMessage, buildFeedbackContextFromSearchParams, buildFeedbackDiagnostics, } from "../ReportProblemValidators.js";
|
|
12
|
+
import { parseDraftRecord, parseDraftSummary, parseEnvelopeReport, getErrorMessage, buildFeedbackContextFromSearchParams, buildFeedbackDiagnostics, deriveFeedbackArea, } from "../ReportProblemValidators.js";
|
|
12
13
|
const INITIAL_FORM = {
|
|
13
14
|
type: "bug",
|
|
14
|
-
severity: undefined,
|
|
15
15
|
title: "",
|
|
16
16
|
description: "",
|
|
17
17
|
stepsToReproduce: "",
|
|
18
18
|
expectedBehavior: "",
|
|
19
19
|
actualBehavior: "",
|
|
20
|
+
frequency: undefined,
|
|
21
|
+
blockingLevel: undefined,
|
|
22
|
+
goal: "",
|
|
23
|
+
stuckAt: "",
|
|
24
|
+
job: "",
|
|
25
|
+
currentWorkaround: "",
|
|
26
|
+
sawWhat: "",
|
|
27
|
+
whereSeen: "",
|
|
20
28
|
submitting: false,
|
|
21
29
|
};
|
|
22
30
|
const FEEDBACK_TYPES = ["bug", "confusing", "privacy_concern", "feature_request", "other"];
|
|
23
|
-
const SEVERITY_OPTIONS = ["low", "medium", "high"];
|
|
24
31
|
const VALID_FEEDBACK_TYPES = new Set(FEEDBACK_TYPES);
|
|
25
|
-
const
|
|
32
|
+
const FREQUENCY_OPTIONS = ["always", "often", "sometimes", "once"];
|
|
33
|
+
const BLOCKING_OPTIONS = ["blocked", "workaround", "minor"];
|
|
34
|
+
const VALID_FREQUENCIES = new Set(FREQUENCY_OPTIONS);
|
|
35
|
+
const VALID_BLOCKING = new Set(BLOCKING_OPTIONS);
|
|
36
|
+
// 每类型对应的"补充细节"条件字段(spec §6)。标题与描述恒显;其余按 type 折叠。
|
|
37
|
+
const PER_TYPE_DETAIL_FIELDS = {
|
|
38
|
+
bug: ["stepsToReproduce", "expectedBehavior", "actualBehavior", "frequency", "blockingLevel"],
|
|
39
|
+
confusing: ["goal", "stuckAt", "blockingLevel"],
|
|
40
|
+
feature_request: ["job", "currentWorkaround"],
|
|
41
|
+
privacy_concern: ["sawWhat", "whereSeen"],
|
|
42
|
+
other: [],
|
|
43
|
+
};
|
|
44
|
+
// 文本类字段的 i18n 标签键(select 类频率/阻塞度单独处理)。
|
|
45
|
+
const DETAIL_FIELD_LABEL = {
|
|
46
|
+
stepsToReproduce: "pages.reportProblem.form.stepsLabel",
|
|
47
|
+
expectedBehavior: "pages.reportProblem.form.expectedLabel",
|
|
48
|
+
actualBehavior: "pages.reportProblem.form.actualLabel",
|
|
49
|
+
goal: "pages.reportProblem.form.goalLabel",
|
|
50
|
+
stuckAt: "pages.reportProblem.form.stuckAtLabel",
|
|
51
|
+
job: "pages.reportProblem.form.jobLabel",
|
|
52
|
+
currentWorkaround: "pages.reportProblem.form.currentWorkaroundLabel",
|
|
53
|
+
sawWhat: "pages.reportProblem.form.sawWhatLabel",
|
|
54
|
+
whereSeen: "pages.reportProblem.form.whereSeenLabel",
|
|
55
|
+
};
|
|
56
|
+
const CHANNEL_ORDER = ["ingest", "github", "email", "file"];
|
|
57
|
+
// 类型默认值按来源推断(spec §6):failed-tasks / error 入口 → bug,通用入口 → other。
|
|
58
|
+
function defaultTypeFromParams(searchParams) {
|
|
59
|
+
const context = buildFeedbackContextFromSearchParams(searchParams);
|
|
60
|
+
const area = deriveFeedbackArea(context);
|
|
61
|
+
if (area === "failed_tasks" || area === "error")
|
|
62
|
+
return "bug";
|
|
63
|
+
return "other";
|
|
64
|
+
}
|
|
26
65
|
// ── Runtime validator for drafts list envelope (H section / ERR-001/005/009) ─
|
|
27
66
|
function isRecord(value) {
|
|
28
67
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -58,33 +97,89 @@ function PrivacySection({ draft }) {
|
|
|
58
97
|
}
|
|
59
98
|
return (_jsxs("section", { className: "mt-8", "aria-labelledby": "section-privacy", children: [_jsx(SectionTitle, { id: "section-privacy", children: t("pages.reportProblem.privacy.title") }), _jsx("p", { className: "text-ink-3 text-[13px] leading-relaxed mb-4", children: t("pages.reportProblem.privacy.description") }), _jsxs("div", { className: "bg-panel border border-line rounded-[6px] p-4 space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("pages.reportProblem.privacy.included") }), draft.privacy.includedSections.length > 0 ? (_jsx("ul", { className: "space-y-1", children: draft.privacy.includedSections.map((section, i) => (_jsxs("li", { className: "flex items-center gap-2 text-ink-2 text-[13px]", children: [_jsx("span", { className: "text-green-600", "aria-hidden": "true", children: "\u2713" }), section] }, i))) })) : (_jsx("p", { className: "text-ink-4 text-[13px]", children: "\u2014" }))] }), _jsxs("div", { children: [_jsx("h3", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("pages.reportProblem.privacy.excluded") }), draft.privacy.excludedByDefault.length > 0 ? (_jsx("ul", { className: "space-y-1", children: draft.privacy.excludedByDefault.map((section, i) => (_jsxs("li", { className: "flex items-center gap-2 text-ink-2 text-[13px]", children: [_jsx("span", { className: "text-red-500", "aria-hidden": "true", children: "\u2717" }), section] }, i))) })) : (_jsx("p", { className: "text-ink-4 text-[13px]", children: "\u2014" }))] }), draft.privacy.redactionNotes.length > 0 && (_jsxs("div", { children: [_jsx("h3", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("pages.reportProblem.privacy.notes") }), _jsx("ul", { className: "space-y-1", children: draft.privacy.redactionNotes.map((note, i) => (_jsxs("li", { className: "text-ink-2 text-[13px]", children: ["\u2022 ", note] }, i))) })] }))] }), _jsx(NoAutoUploadBanner, {})] }));
|
|
60
99
|
}
|
|
100
|
+
function StatusBadge({ draft }) {
|
|
101
|
+
const { t } = useTranslation();
|
|
102
|
+
const submitted = draft.status === "submitted";
|
|
103
|
+
return (_jsxs("span", { className: submitted
|
|
104
|
+
? "inline-flex items-center gap-1 px-2 py-[2px] rounded-[3px] text-[11px] font-medium border border-green-200 bg-green-50 text-green-700"
|
|
105
|
+
: "inline-flex items-center gap-1 px-2 py-[2px] rounded-[3px] text-[11px] font-medium border border-line bg-surface text-ink-3", children: [_jsx("span", { className: submitted ? "w-1.5 h-1.5 rounded-full bg-green-500" : "w-1.5 h-1.5 rounded-full bg-ink-3", "aria-hidden": "true" }), submitted ? t("pages.reportProblem.draft.sentBadge") : t("pages.reportProblem.draft.draftBadge")] }));
|
|
106
|
+
}
|
|
61
107
|
function DraftCard({ draft, onCopyMarkdown, onCopyEmail, onOpenGithub, onOpenEmail, onDelete }) {
|
|
62
108
|
const { t } = useTranslation();
|
|
63
109
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
64
|
-
return (_jsxs("article", { className: "bg-panel border border-line rounded-[6px] p-4", children: [_jsx("div", { className: "font-mono text-[11px] text-ink-3
|
|
110
|
+
return (_jsxs("article", { className: "bg-panel border border-line rounded-[6px] p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 mb-2", children: [_jsx("div", { className: "font-mono text-[11px] text-ink-3", children: t("pages.reportProblem.draft.savedAt", { id: draft.id, at: draft.createdAt }) }), _jsx(StatusBadge, { draft: draft })] }), _jsx("div", { className: "font-semibold text-ink mb-1", children: draft.title }), (draft.status === "submitted" || draft.trackingId || draft.submittedVia) && (_jsxs("div", { className: "text-ink-3 text-[12px] mb-2 space-y-0.5", children: [draft.submittedVia && (_jsxs("div", { children: ["via ", draft.submittedVia, draft.submittedAt ? ` · ${draft.submittedAt}` : ""] })), draft.trackingId && (_jsxs("div", { className: "font-mono", children: [t("pages.reportProblem.channel.receiptTrackingId"), ": ", draft.trackingId] })), draft.externalUrl && (_jsx("div", { children: _jsx("a", { href: draft.externalUrl, target: "_blank", rel: "noreferrer noopener", className: "text-gov underline hover:text-gov-2", children: t("pages.reportProblem.channel.openIssue") }) }))] })), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx("button", { type: "button", onClick: onCopyMarkdown, className: "border border-line bg-surface text-ink rounded-[3px] px-[12px] py-[5px] text-[12px] hover:border-line-2 transition-colors focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.reportProblem.draft.copyMarkdown") }), _jsx("button", { type: "button", onClick: onCopyEmail, className: "border border-line bg-surface text-ink rounded-[3px] px-[12px] py-[5px] text-[12px] hover:border-line-2 transition-colors focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.reportProblem.draft.copyEmail") }), draft.outputs.githubIssueUrl && (_jsx("button", { type: "button", onClick: onOpenGithub, className: "border border-line bg-surface text-ink rounded-[3px] px-[12px] py-[5px] text-[12px] hover:border-line-2 transition-colors focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.reportProblem.draft.openGithub") })), draft.outputs.mailtoUrl && (_jsx("button", { type: "button", onClick: onOpenEmail, className: "border border-line bg-surface text-ink rounded-[3px] px-[12px] py-[5px] text-[12px] hover:border-line-2 transition-colors focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.reportProblem.draft.openEmail") })), !confirmDelete ? (_jsx("button", { type: "button", onClick: () => setConfirmDelete(true), className: "border border-red-200 text-red-600 rounded-[3px] px-[12px] py-[5px] text-[12px] hover:bg-red-50 transition-colors focus-visible:outline-2 focus-visible:outline-red-400 focus-visible:outline-offset-2", children: t("pages.reportProblem.draft.delete") })) : (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: onDelete, className: "border border-red-400 bg-red-600 text-white rounded-[3px] px-[12px] py-[5px] text-[12px] hover:bg-red-700 transition-colors focus-visible:outline-2 focus-visible:outline-red-400 focus-visible:outline-offset-2", children: [t("pages.reportProblem.draft.delete"), "?"] }), _jsx("button", { type: "button", onClick: () => setConfirmDelete(false), className: "border border-line bg-surface text-ink rounded-[3px] px-[12px] py-[5px] text-[12px] hover:border-line-2 transition-colors", children: "\u00D7" })] }))] })] }));
|
|
65
111
|
}
|
|
66
112
|
function SavedDraftsSection({ drafts, onLoad, onDelete }) {
|
|
67
113
|
const { t } = useTranslation();
|
|
68
|
-
return (_jsxs("section", { className: "mt-8", "aria-labelledby": "section-saved", children: [_jsx(SectionTitle, { id: "section-saved", children: t("pages.reportProblem.saved.title") }), drafts.length === 0 ? (_jsx("p", { className: "text-ink-3 text-[13px] leading-relaxed py-2", children: "\u2014" })) : (_jsx("div", { className: "space-y-3", children: drafts.map((draft) => (_jsxs("article", { className: "bg-panel border border-line rounded-[6px] px-4 py-3 flex items-center justify-between gap-4", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "font-mono text-[11px] text-ink-3
|
|
114
|
+
return (_jsxs("section", { className: "mt-8", "aria-labelledby": "section-saved", children: [_jsx(SectionTitle, { id: "section-saved", children: t("pages.reportProblem.saved.title") }), drafts.length === 0 ? (_jsx("p", { className: "text-ink-3 text-[13px] leading-relaxed py-2", children: "\u2014" })) : (_jsx("div", { className: "space-y-3", children: drafts.map((draft) => (_jsxs("article", { className: "bg-panel border border-line rounded-[6px] px-4 py-3 flex items-center justify-between gap-4", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2 mb-1", children: [_jsxs("div", { className: "font-mono text-[11px] text-ink-3", children: [enumLabel('feedbackType', draft.type, t), " \u00B7 ", draft.createdAt] }), draft.status === "submitted" && (_jsx("span", { className: "inline-flex items-center gap-1 px-1.5 py-[1px] rounded-[2px] text-[10px] font-medium border border-green-200 bg-green-50 text-green-700", children: t("pages.reportProblem.draft.sentBadge") }))] }), _jsx("div", { className: "text-ink text-sm truncate", children: draft.title })] }), _jsxs("div", { className: "flex gap-2 shrink-0", children: [_jsx("button", { type: "button", onClick: () => onLoad(draft.id), className: "border border-gov bg-gov text-paper rounded-[3px] px-[14px] py-[6px] text-[12.5px] font-medium hover:bg-gov-2 transition-colors focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.reportProblem.saved.load") }), _jsx("button", { type: "button", onClick: () => onDelete(draft.id), className: "border border-red-200 text-red-600 rounded-[3px] px-[10px] py-[6px] text-[12px] hover:bg-red-50 transition-colors focus-visible:outline-2 focus-visible:outline-red-400 focus-visible:outline-offset-2", children: t("pages.reportProblem.draft.delete") })] })] }, draft.id))) }))] }));
|
|
115
|
+
}
|
|
116
|
+
// ── Channel ladder (Slice 3, spec §4/§11) ─────────────────────────────────────
|
|
117
|
+
function ChannelSubmitArea({ draft, channels, onSubmitIngest, onSubmitGithub, onOpenEmail, onExportFile, onMarkSent }) {
|
|
118
|
+
const { t } = useTranslation();
|
|
119
|
+
const byId = useMemo(() => {
|
|
120
|
+
const m = new Map();
|
|
121
|
+
for (const c of channels ?? [])
|
|
122
|
+
m.set(c.id, c);
|
|
123
|
+
return m;
|
|
124
|
+
}, [channels]);
|
|
125
|
+
const submitted = draft.status === "submitted";
|
|
126
|
+
const renderChannelButton = (id) => {
|
|
127
|
+
const status = byId.get(id);
|
|
128
|
+
const available = status?.available ?? false;
|
|
129
|
+
// email: 客户端动作,但依赖 maintainer_email 已配置(占位地址不可投递,
|
|
130
|
+
// 探测不可用时禁用并内联显示原因 + nextAction,与 ingest/github 一致)。
|
|
131
|
+
if (id === "email") {
|
|
132
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx("button", { type: "button", disabled: !available, onClick: onOpenEmail, className: available
|
|
133
|
+
? "w-full border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[8px] text-[13px] hover:border-line-2 transition-colors focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
134
|
+
: "w-full border border-line bg-surface text-ink-4 rounded-[3px] px-[14px] py-[8px] text-[13px] cursor-not-allowed", children: t("pages.reportProblem.channel.email") }), !available && (status?.reason || status?.nextAction) && (_jsxs("p", { className: "text-[11px] text-ink-3 leading-snug", children: [t("pages.reportProblem.channel.disablingReason", { reason: status?.reason ?? "" }), status?.nextAction ? ` · ${status.nextAction}` : ""] })), !submitted && (_jsx("button", { type: "button", onClick: () => onMarkSent("email"), className: "text-[11px] text-ink-3 hover:text-gov underline underline-offset-2 text-left", children: t("pages.reportProblem.draft.markAsSent") }))] }, "email"));
|
|
135
|
+
}
|
|
136
|
+
if (id === "file") {
|
|
137
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx("button", { type: "button", onClick: onExportFile, className: "w-full border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[8px] text-[13px] hover:border-line-2 transition-colors focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.reportProblem.channel.file") }), !submitted && (_jsx("button", { type: "button", onClick: () => onMarkSent("file"), className: "text-[11px] text-ink-3 hover:text-gov underline underline-offset-2 text-left", children: t("pages.reportProblem.draft.markAsSent") }))] }, "file"));
|
|
138
|
+
}
|
|
139
|
+
// ingest / github: 服务端通道,需探测可用性(rc-9: 禁用显示原因 + nextAction)。
|
|
140
|
+
const isIngest = id === "ingest";
|
|
141
|
+
const label = isIngest ? t("pages.reportProblem.channel.ingest") : t("pages.reportProblem.channel.github");
|
|
142
|
+
const onClick = isIngest ? onSubmitIngest : onSubmitGithub;
|
|
143
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx("button", { type: "button", disabled: !available, onClick: onClick, className: available
|
|
144
|
+
? "w-full border border-gov bg-gov text-paper rounded-[3px] px-[14px] py-[8px] text-[13px] font-medium hover:bg-gov-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2"
|
|
145
|
+
: "w-full border border-line bg-surface text-ink-4 rounded-[3px] px-[14px] py-[8px] text-[13px] cursor-not-allowed", children: label }), !available && (status?.reason || status?.nextAction) && (_jsxs("p", { className: "text-[11px] text-ink-3 leading-snug", children: [t("pages.reportProblem.channel.disablingReason", { reason: status?.reason ?? "" }), status?.nextAction ? ` · ${status.nextAction}` : ""] }))] }, id));
|
|
146
|
+
};
|
|
147
|
+
return (_jsxs("section", { className: "mt-8", "aria-labelledby": "section-channel", children: [_jsx(SectionTitle, { id: "section-channel", children: t("pages.reportProblem.channel.title") }), _jsx("p", { className: "text-ink-3 text-[13px] leading-relaxed mb-4", children: t("pages.reportProblem.channel.description") }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3", children: CHANNEL_ORDER.map(renderChannelButton) })] }));
|
|
148
|
+
}
|
|
149
|
+
function ConfirmSubmitDialog({ draft, channel, open, busy, onConfirm, onCancel }) {
|
|
150
|
+
const { t } = useTranslation();
|
|
151
|
+
return (_jsx(Dialog, { open: open, onOpenChange: (o) => { if (!o && !busy)
|
|
152
|
+
onCancel(); }, children: _jsxs(DialogContent, { className: "sm:max-w-xl max-h-[85vh] overflow-hidden flex flex-col", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t("pages.reportProblem.channel.confirmTitle") }), _jsx(DialogDescription, { children: channel === "ingest"
|
|
153
|
+
? t("pages.reportProblem.channel.confirmDescription")
|
|
154
|
+
: t("pages.reportProblem.channel.github") })] }), _jsxs("div", { className: "flex-1 min-h-0 overflow-auto space-y-2", children: [_jsx("div", { className: "font-mono text-[12px] text-ink-3", children: draft.title }), _jsx("h3", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3", children: t("pages.reportProblem.channel.confirmBodyLabel") }), _jsx("pre", { className: "whitespace-pre-wrap text-ink-2 text-[13px] leading-relaxed bg-surface border border-line rounded-[3px] p-3 max-h-[320px] overflow-auto", children: draft.outputs.markdown })] }), _jsxs(DialogFooter, { children: [_jsx("button", { type: "button", onClick: onCancel, disabled: busy, className: "border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 disabled:opacity-50", children: t("pages.reportProblem.channel.confirmCancel") }), _jsx("button", { type: "button", onClick: onConfirm, disabled: busy, className: "border border-gov bg-gov text-paper rounded-[3px] px-[14px] py-[6px] text-[12.5px] font-medium hover:bg-gov-2 disabled:opacity-50 disabled:cursor-not-allowed", children: busy ? t("pages.reportProblem.form.submitting") : t("pages.reportProblem.channel.confirmSubmit") })] })] }) }));
|
|
69
155
|
}
|
|
70
156
|
export function ReportProblemPage() {
|
|
71
157
|
const { t } = useTranslation();
|
|
72
158
|
const [searchParams] = useSearchParams();
|
|
73
|
-
const [form, setForm] = useState(
|
|
159
|
+
const [form, setForm] = useState(() => ({
|
|
160
|
+
...INITIAL_FORM,
|
|
161
|
+
type: defaultTypeFromParams(searchParams),
|
|
162
|
+
}));
|
|
74
163
|
const [drafts, setDrafts] = useState([]);
|
|
75
164
|
const [currentDraft, setCurrentDraft] = useState(null);
|
|
76
165
|
const [loadingState, setLoadingState] = useState("loading");
|
|
77
166
|
const [loadError, setLoadError] = useState(null);
|
|
78
167
|
const [showPreview, setShowPreview] = useState(false);
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
168
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
169
|
+
// ── Channel ladder state (Slice 3) ──
|
|
170
|
+
const [channels, setChannels] = useState(null);
|
|
171
|
+
const [confirmCtx, setConfirmCtx] = useState(null);
|
|
172
|
+
const [submittingChannel, setSubmittingChannel] = useState(false);
|
|
82
173
|
const contextFromUrl = useMemo(() => buildFeedbackContextFromSearchParams(searchParams), [searchParams]);
|
|
83
|
-
|
|
174
|
+
const areaFromUrl = useMemo(() => deriveFeedbackArea(contextFromUrl), [contextFromUrl]);
|
|
175
|
+
// 类型切换时:有细节字段的类型自动展开"补充细节"。
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
setShowDetails(PER_TYPE_DETAIL_FIELDS[form.type].length > 0);
|
|
178
|
+
}, [form.type]);
|
|
179
|
+
// ── Load drafts + channels on mount ─────────────────────────────────────
|
|
84
180
|
const loadDrafts = useCallback(async () => {
|
|
85
181
|
const result = await listFeedbackReports();
|
|
86
182
|
if (!result.success) {
|
|
87
|
-
// ERR-002: graceful degradation with reason
|
|
88
183
|
setLoadError(result.error ?? "Failed to load drafts");
|
|
89
184
|
setLoadingState("loaded");
|
|
90
185
|
return;
|
|
@@ -100,13 +195,45 @@ export function ReportProblemPage() {
|
|
|
100
195
|
setLoadError(null);
|
|
101
196
|
setLoadingState("loaded");
|
|
102
197
|
}, []);
|
|
198
|
+
const loadChannels = useCallback(async () => {
|
|
199
|
+
const result = await fetchFeedbackChannels();
|
|
200
|
+
if (!result.success || !result.data) {
|
|
201
|
+
setChannels(null);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
setChannels(result.data.channels);
|
|
205
|
+
}, []);
|
|
103
206
|
useEffect(() => {
|
|
104
207
|
loadDrafts();
|
|
105
|
-
|
|
208
|
+
loadChannels();
|
|
209
|
+
}, [loadDrafts, loadChannels]);
|
|
106
210
|
// ── Form handlers ────────────────────────────────────────────────────────
|
|
107
211
|
const handleFieldChange = useCallback((field, value) => {
|
|
108
212
|
setForm((prev) => ({ ...prev, [field]: value }));
|
|
109
213
|
}, []);
|
|
214
|
+
const buildInput = useCallback(() => {
|
|
215
|
+
const base = {
|
|
216
|
+
type: form.type,
|
|
217
|
+
title: form.title.trim(),
|
|
218
|
+
description: form.description.trim(),
|
|
219
|
+
stepsToReproduce: form.stepsToReproduce.trim() || undefined,
|
|
220
|
+
expectedBehavior: form.expectedBehavior.trim() || undefined,
|
|
221
|
+
actualBehavior: form.actualBehavior.trim() || undefined,
|
|
222
|
+
// 类型化新字段(全部可选,按 type 条件提交)
|
|
223
|
+
goal: form.goal.trim() || undefined,
|
|
224
|
+
stuckAt: form.stuckAt.trim() || undefined,
|
|
225
|
+
job: form.job.trim() || undefined,
|
|
226
|
+
currentWorkaround: form.currentWorkaround.trim() || undefined,
|
|
227
|
+
sawWhat: form.sawWhat.trim() || undefined,
|
|
228
|
+
whereSeen: form.whereSeen.trim() || undefined,
|
|
229
|
+
frequency: form.frequency ?? undefined,
|
|
230
|
+
blockingLevel: form.blockingLevel ?? undefined,
|
|
231
|
+
...(areaFromUrl ? { area: areaFromUrl } : {}),
|
|
232
|
+
...(contextFromUrl?.taskId ? { taskId: contextFromUrl.taskId } : {}),
|
|
233
|
+
...(contextFromUrl ? { context: contextFromUrl } : {}),
|
|
234
|
+
};
|
|
235
|
+
return base;
|
|
236
|
+
}, [form, areaFromUrl, contextFromUrl]);
|
|
110
237
|
const handleSubmit = useCallback(async (e) => {
|
|
111
238
|
e.preventDefault();
|
|
112
239
|
if (!form.title.trim() || !form.description.trim()) {
|
|
@@ -115,64 +242,37 @@ export function ReportProblemPage() {
|
|
|
115
242
|
}
|
|
116
243
|
setForm((prev) => ({ ...prev, submitting: true }));
|
|
117
244
|
try {
|
|
118
|
-
const input = {
|
|
119
|
-
type: form.type,
|
|
120
|
-
title: form.title.trim(),
|
|
121
|
-
description: form.description.trim(),
|
|
122
|
-
stepsToReproduce: form.stepsToReproduce.trim() || undefined,
|
|
123
|
-
expectedBehavior: form.expectedBehavior.trim() || undefined,
|
|
124
|
-
actualBehavior: form.actualBehavior.trim() || undefined,
|
|
125
|
-
severity: form.severity,
|
|
126
|
-
// P0-1: 传递顶层 taskId 触发 agentDraft 合并(Task 13)
|
|
127
|
-
// contextFromUrl.taskId 来自 URL query(FailedTasksPage 跳转时传入)
|
|
128
|
-
// createFeedbackReport 检查 draft.taskId(顶层)决定是否从 pending_agent_drafts 表读取
|
|
129
|
-
...(contextFromUrl?.taskId ? { taskId: contextFromUrl.taskId } : {}),
|
|
130
|
-
...(contextFromUrl ? { context: contextFromUrl } : {}),
|
|
131
|
-
};
|
|
132
|
-
// ── Collect diagnostics concurrently (Task 5) ───────────────────────
|
|
133
|
-
// Fetch three APIs in parallel; Promise.allSettled ensures a single
|
|
134
|
-
// failure doesn't block the others. buildFeedbackDiagnostics then
|
|
135
|
-
// assembles a diagnostics object with unavailableReason for each failed
|
|
136
|
-
// field (rc-9-no-silent-fallback).
|
|
137
245
|
const [configSettled, lifecycleSettled, healthSettled] = await Promise.allSettled([
|
|
138
246
|
fetchConfigSummary(),
|
|
139
247
|
request('/api/v1/lifecycle/state'),
|
|
140
248
|
request('/api/health'),
|
|
141
249
|
]);
|
|
142
250
|
const diagnostics = buildFeedbackDiagnostics(configSettled, lifecycleSettled, healthSettled);
|
|
143
|
-
const result = await createFeedbackReport(
|
|
251
|
+
const result = await createFeedbackReport(buildInput(), diagnostics);
|
|
144
252
|
if (!result.success) {
|
|
145
253
|
toast.error(getErrorMessage(result, t("pages.reportProblem.errors.createFailed")));
|
|
146
254
|
setForm((prev) => ({ ...prev, submitting: false }));
|
|
147
255
|
return;
|
|
148
256
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// Try parseDraftRecord directly as fallback — no `as` cast (ERR-001/005)
|
|
153
|
-
let directParsed = null;
|
|
154
|
-
if (isRecord(result.data) && Object.hasOwn(result.data, "report")) {
|
|
155
|
-
const reportValue = result.data["report"];
|
|
156
|
-
directParsed = parseDraftRecord(reportValue);
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
directParsed = parseDraftRecord(result.data);
|
|
160
|
-
}
|
|
161
|
-
if (directParsed === null) {
|
|
162
|
-
toast.error(t("pages.reportProblem.errors.createFailed"));
|
|
163
|
-
setForm((prev) => ({ ...prev, submitting: false }));
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
setCurrentDraft(directParsed);
|
|
257
|
+
let parsed = parseEnvelopeReport(result.data);
|
|
258
|
+
if (parsed === null && isRecord(result.data) && Object.hasOwn(result.data, "report")) {
|
|
259
|
+
parsed = parseDraftRecord(result.data["report"]);
|
|
167
260
|
}
|
|
168
|
-
|
|
169
|
-
|
|
261
|
+
if (parsed === null) {
|
|
262
|
+
toast.error(t("pages.reportProblem.errors.createFailed"));
|
|
263
|
+
setForm((prev) => ({ ...prev, submitting: false }));
|
|
264
|
+
return;
|
|
170
265
|
}
|
|
266
|
+
setCurrentDraft(parsed);
|
|
171
267
|
toast.success(t("pages.reportProblem.toast.draftCreated"));
|
|
172
|
-
|
|
173
|
-
|
|
268
|
+
setForm((prev) => ({
|
|
269
|
+
...INITIAL_FORM,
|
|
270
|
+
type: prev.type,
|
|
271
|
+
frequency: undefined,
|
|
272
|
+
blockingLevel: undefined,
|
|
273
|
+
submitting: false,
|
|
274
|
+
}));
|
|
174
275
|
setShowPreview(false);
|
|
175
|
-
// Reload drafts list
|
|
176
276
|
await loadDrafts();
|
|
177
277
|
}
|
|
178
278
|
catch {
|
|
@@ -181,7 +281,7 @@ export function ReportProblemPage() {
|
|
|
181
281
|
finally {
|
|
182
282
|
setForm((prev) => ({ ...prev, submitting: false }));
|
|
183
283
|
}
|
|
184
|
-
}, [form, t, loadDrafts,
|
|
284
|
+
}, [form, t, loadDrafts, buildInput]);
|
|
185
285
|
// ── Draft actions ────────────────────────────────────────────────────────
|
|
186
286
|
const handleLoadDraft = useCallback(async (id) => {
|
|
187
287
|
const result = await getFeedbackReport(id);
|
|
@@ -197,17 +297,26 @@ export function ReportProblemPage() {
|
|
|
197
297
|
setCurrentDraft(parsed);
|
|
198
298
|
setForm({
|
|
199
299
|
type: parsed.type,
|
|
200
|
-
severity: parsed.userText.userSeverity,
|
|
201
300
|
title: parsed.title,
|
|
202
301
|
description: parsed.userText.description,
|
|
203
302
|
stepsToReproduce: parsed.userText.stepsToReproduce ?? "",
|
|
204
303
|
expectedBehavior: parsed.userText.expectedBehavior ?? "",
|
|
205
304
|
actualBehavior: parsed.userText.actualBehavior ?? "",
|
|
305
|
+
frequency: parsed.userText.frequency,
|
|
306
|
+
blockingLevel: parsed.userText.blockingLevel,
|
|
307
|
+
goal: parsed.userText.goal ?? "",
|
|
308
|
+
stuckAt: parsed.userText.stuckAt ?? "",
|
|
309
|
+
job: parsed.userText.job ?? "",
|
|
310
|
+
currentWorkaround: parsed.userText.currentWorkaround ?? "",
|
|
311
|
+
sawWhat: parsed.userText.sawWhat ?? "",
|
|
312
|
+
whereSeen: parsed.userText.whereSeen ?? "",
|
|
206
313
|
submitting: false,
|
|
207
314
|
});
|
|
208
315
|
}, [t]);
|
|
209
316
|
const handleCopyMarkdown = useCallback(async (draft) => {
|
|
210
317
|
try {
|
|
318
|
+
if (!draft.outputs.markdown)
|
|
319
|
+
return;
|
|
211
320
|
await navigator.clipboard.writeText(draft.outputs.markdown);
|
|
212
321
|
toast.success(t("pages.reportProblem.toast.markdownCopied"));
|
|
213
322
|
}
|
|
@@ -217,6 +326,8 @@ export function ReportProblemPage() {
|
|
|
217
326
|
}, [t]);
|
|
218
327
|
const handleCopyEmail = useCallback(async (draft) => {
|
|
219
328
|
try {
|
|
329
|
+
if (!draft.outputs.emailText)
|
|
330
|
+
return;
|
|
220
331
|
await navigator.clipboard.writeText(draft.outputs.emailText);
|
|
221
332
|
toast.success(t("pages.reportProblem.toast.emailCopied"));
|
|
222
333
|
}
|
|
@@ -234,6 +345,70 @@ export function ReportProblemPage() {
|
|
|
234
345
|
window.open(draft.outputs.mailtoUrl, "_blank");
|
|
235
346
|
}
|
|
236
347
|
}, []);
|
|
348
|
+
const handleExportFile = useCallback((draft) => {
|
|
349
|
+
const blob = new Blob([draft.outputs.markdown], { type: "text/markdown;charset=utf-8" });
|
|
350
|
+
const url = URL.createObjectURL(blob);
|
|
351
|
+
const a = document.createElement("a");
|
|
352
|
+
a.href = url;
|
|
353
|
+
a.download = `PD-feedback-${draft.id}.md`;
|
|
354
|
+
document.body.appendChild(a);
|
|
355
|
+
a.click();
|
|
356
|
+
a.remove();
|
|
357
|
+
URL.revokeObjectURL(url);
|
|
358
|
+
}, []);
|
|
359
|
+
const refreshDraft = useCallback(async (id) => {
|
|
360
|
+
const result = await getFeedbackReport(id);
|
|
361
|
+
if (result.success) {
|
|
362
|
+
const parsed = parseEnvelopeReport(result.data);
|
|
363
|
+
if (parsed)
|
|
364
|
+
setCurrentDraft(parsed);
|
|
365
|
+
}
|
|
366
|
+
}, []);
|
|
367
|
+
const handleSubmitChannel = useCallback(async (channel) => {
|
|
368
|
+
const draft = currentDraft;
|
|
369
|
+
if (!draft)
|
|
370
|
+
return;
|
|
371
|
+
setSubmittingChannel(true);
|
|
372
|
+
try {
|
|
373
|
+
const result = await submitFeedbackReport(draft.id, channel);
|
|
374
|
+
if (!result.success) {
|
|
375
|
+
toast.error(getErrorMessage(result, t("pages.reportProblem.toast.submitFailed")));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
await refreshDraft(draft.id);
|
|
379
|
+
await loadDrafts();
|
|
380
|
+
const data = result.data;
|
|
381
|
+
if (data?.writeBackFailed) {
|
|
382
|
+
toast.error(t("pages.reportProblem.channel.writeBackNote"));
|
|
383
|
+
}
|
|
384
|
+
else if (data?.alreadySubmitted) {
|
|
385
|
+
toast.info(t("pages.reportProblem.channel.alreadySubmitted"));
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
toast.success(t("pages.reportProblem.toast.submitSucceeded", { trackingId: data?.trackingId ?? "" }));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
toast.error(t("pages.reportProblem.toast.submitFailed"));
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
setSubmittingChannel(false);
|
|
396
|
+
setConfirmCtx(null);
|
|
397
|
+
}
|
|
398
|
+
}, [currentDraft, t, refreshDraft, loadDrafts]);
|
|
399
|
+
const handleMarkSent = useCallback(async (via) => {
|
|
400
|
+
const draft = currentDraft;
|
|
401
|
+
if (!draft)
|
|
402
|
+
return;
|
|
403
|
+
const result = await markFeedbackReportSent(draft.id, via);
|
|
404
|
+
if (!result.success) {
|
|
405
|
+
toast.error(getErrorMessage(result, t("pages.reportProblem.toast.submitFailed")));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
toast.success(t("pages.reportProblem.toast.markSentSucceeded"));
|
|
409
|
+
await refreshDraft(draft.id);
|
|
410
|
+
await loadDrafts();
|
|
411
|
+
}, [currentDraft, t, refreshDraft, loadDrafts]);
|
|
237
412
|
const handleDeleteDraft = useCallback(async (id) => {
|
|
238
413
|
const result = await deleteFeedbackReport(id);
|
|
239
414
|
if (!result.success) {
|
|
@@ -254,21 +429,39 @@ export function ReportProblemPage() {
|
|
|
254
429
|
if (loadingState === "error") {
|
|
255
430
|
return (_jsxs(PageShell, { children: [_jsx("div", { className: "font-mono text-[12px] tracking-[0.14em] text-ink-3 uppercase mb-3", children: t("pages.reportProblem.eyebrow") }), _jsx("h1", { className: "text-[29px] font-semibold tracking-tight text-ink mb-2", children: t("pages.reportProblem.title") }), _jsxs("div", { className: "mt-6 p-4 bg-panel border border-line rounded-[6px] text-ink-2 text-sm", children: [_jsx("p", { children: t("pages.reportProblem.loadError") }), loadError && (_jsx("p", { className: "mt-2 text-ink-4 text-[13px] font-mono", children: loadError }))] })] }));
|
|
256
431
|
}
|
|
432
|
+
const detailFields = PER_TYPE_DETAIL_FIELDS[form.type];
|
|
433
|
+
const renderDetailField = (key) => {
|
|
434
|
+
if (key === "frequency") {
|
|
435
|
+
const label = t("pages.reportProblem.form.frequencyLabel");
|
|
436
|
+
return (_jsxs("div", { children: [_jsx("label", { htmlFor: "feedback-frequency", className: "block text-ink-2 text-[13px] mb-1.5", children: label }), _jsxs("select", { id: "feedback-frequency", value: form.frequency ?? "", onChange: (e) => {
|
|
437
|
+
const v = e.target.value;
|
|
438
|
+
if (v === "")
|
|
439
|
+
handleFieldChange("frequency", undefined);
|
|
440
|
+
else if (VALID_FREQUENCIES.has(v))
|
|
441
|
+
handleFieldChange("frequency", v);
|
|
442
|
+
}, className: "border border-line bg-surface rounded-[3px] px-3 py-2 text-sm text-ink focus:outline-none focus:border-gov", children: [_jsx("option", { value: "", children: t("pages.reportProblem.form.frequencyNotSet") }), FREQUENCY_OPTIONS.map((f) => (_jsx("option", { value: f, children: t(`pages.reportProblem.form.frequency${f.charAt(0).toUpperCase() + f.slice(1)}`) }, f)))] })] }, "frequency"));
|
|
443
|
+
}
|
|
444
|
+
if (key === "blockingLevel") {
|
|
445
|
+
const label = t("pages.reportProblem.form.blockingLevelLabel");
|
|
446
|
+
return (_jsxs("div", { children: [_jsx("label", { htmlFor: "feedback-blocking", className: "block text-ink-2 text-[13px] mb-1.5", children: label }), _jsxs("select", { id: "feedback-blocking", value: form.blockingLevel ?? "", onChange: (e) => {
|
|
447
|
+
const v = e.target.value;
|
|
448
|
+
if (v === "")
|
|
449
|
+
handleFieldChange("blockingLevel", undefined);
|
|
450
|
+
else if (VALID_BLOCKING.has(v))
|
|
451
|
+
handleFieldChange("blockingLevel", v);
|
|
452
|
+
}, className: "border border-line bg-surface rounded-[3px] px-3 py-2 text-sm text-ink focus:outline-none focus:border-gov", children: [_jsx("option", { value: "", children: t("pages.reportProblem.form.blockingLevelNotSet") }), BLOCKING_OPTIONS.map((b) => (_jsx("option", { value: b, children: t(`pages.reportProblem.form.blockingLevel${b.charAt(0).toUpperCase() + b.slice(1)}`) }, b)))] })] }, "blockingLevel"));
|
|
453
|
+
}
|
|
454
|
+
const value = form[key];
|
|
455
|
+
const label = t(DETAIL_FIELD_LABEL[key] ?? "pages.reportProblem.form.descriptionLabel");
|
|
456
|
+
return (_jsxs("div", { children: [_jsx("label", { htmlFor: `feedback-${key}`, className: "block text-ink-2 text-[13px] mb-1.5", children: label }), _jsx("textarea", { id: `feedback-${key}`, value: typeof value === "string" ? value : "", onChange: (e) => handleFieldChange(key, e.target.value), rows: key === "stepsToReproduce" ? 3 : 2, className: "w-full border border-line bg-surface rounded-[3px] px-3 py-2 text-sm text-ink focus:outline-none focus:border-gov focus:ring-1 focus:ring-gov resize-y" })] }, key));
|
|
457
|
+
};
|
|
257
458
|
// ── Loaded state ─────────────────────────────────────────────────────────
|
|
258
459
|
return (_jsx(PageShell, { children: _jsxs("div", { className: "animate-[pdFadeIn_400ms_ease-out]", children: [_jsx("div", { className: "font-mono text-[12px] tracking-[0.14em] text-ink-3 uppercase mb-3", children: t("pages.reportProblem.eyebrow") }), _jsx("h1", { className: "text-[29px] font-semibold tracking-tight text-ink mb-2", children: t("pages.reportProblem.title") }), _jsx("p", { className: "text-ink-3 text-[14px] max-w-[760px] leading-relaxed mb-7", children: t("pages.reportProblem.subtitle") }), _jsx(NoAutoUploadBanner, {}), _jsxs("section", { "aria-labelledby": "section-form", children: [_jsx(SectionTitle, { id: "section-form", children: t("pages.reportProblem.form.title") }), _jsx("p", { className: "text-ink-3 text-[13px] leading-relaxed mb-4", children: t("pages.reportProblem.form.description") }), _jsxs("form", { onSubmit: handleSubmit, className: "bg-panel border border-line rounded-[6px] p-5 space-y-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "feedback-type", className: "block text-ink-2 text-[13px] mb-1.5", children: t("pages.reportProblem.form.type") }), _jsx("select", { id: "feedback-type", value: form.type, onChange: (e) => {
|
|
259
460
|
const val = e.target.value;
|
|
260
461
|
if (VALID_FEEDBACK_TYPES.has(val)) {
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
}, className: "border border-line bg-surface rounded-[3px] px-3 py-2 text-sm text-ink focus:outline-none focus:border-gov", children: FEEDBACK_TYPES.map((ft) => (_jsx("option", { value: ft, children: t(`pages.reportProblem.form.types.${ft}`) }, ft))) })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "feedback-severity", className: "block text-ink-2 text-[13px] mb-1.5", children: t("pages.reportProblem.form.severity") }), _jsxs("select", { id: "feedback-severity", value: form.severity ?? "", onChange: (e) => {
|
|
264
|
-
const val = e.target.value;
|
|
265
|
-
if (val === "") {
|
|
266
|
-
handleFieldChange("severity", undefined);
|
|
267
|
-
}
|
|
268
|
-
else if (VALID_SEVERITY_OPTIONS.has(val)) {
|
|
269
|
-
handleFieldChange("severity", val);
|
|
462
|
+
setForm((prev) => ({ ...prev, type: val, frequency: undefined, blockingLevel: undefined }));
|
|
270
463
|
}
|
|
271
|
-
}, className: "border border-line bg-surface rounded-[3px] px-3 py-2 text-sm text-ink focus:outline-none focus:border-gov", children:
|
|
464
|
+
}, className: "border border-line bg-surface rounded-[3px] px-3 py-2 text-sm text-ink focus:outline-none focus:border-gov", children: FEEDBACK_TYPES.map((ft) => (_jsx("option", { value: ft, children: t(`pages.reportProblem.form.types.${ft}`) }, ft))) })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "feedback-title", className: "block text-ink-2 text-[13px] mb-1.5", children: t("pages.reportProblem.form.titleLabel") }), _jsx("input", { id: "feedback-title", type: "text", value: form.title, onChange: (e) => handleFieldChange("title", e.target.value), className: "w-full border border-line bg-surface rounded-[3px] px-3 py-2 text-sm text-ink focus:outline-none focus:border-gov focus:ring-1 focus:ring-gov" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "feedback-description", className: "block text-ink-2 text-[13px] mb-1.5", children: t("pages.reportProblem.form.descriptionLabel") }), _jsx("textarea", { id: "feedback-description", value: form.description, onChange: (e) => handleFieldChange("description", e.target.value), rows: 4, className: "w-full border border-line bg-surface rounded-[3px] px-3 py-2 text-sm text-ink focus:outline-none focus:border-gov focus:ring-1 focus:ring-gov resize-y" })] }), detailFields.length > 0 && (_jsx("div", { children: _jsx("button", { type: "button", onClick: () => setShowDetails((prev) => !prev), className: "text-gov text-[13px] font-medium underline underline-offset-2 hover:text-gov-2 transition-colors", children: showDetails ? t("pages.reportProblem.form.detailsHide") : t("pages.reportProblem.form.detailsToggle") }) })), showDetails && (_jsx("div", { className: "space-y-4 border-t border-line pt-4", children: detailFields.map(renderDetailField) })), _jsxs("div", { className: "flex items-center gap-3 pt-2", children: [_jsx("button", { type: "submit", disabled: form.submitting, className: "border border-gov bg-gov text-paper rounded-[3px] px-[14px] py-[6px] text-[12.5px] font-medium hover:bg-gov-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: form.submitting ? t("pages.reportProblem.form.submitting") : t("pages.reportProblem.form.submit") }), currentDraft && (_jsx("button", { type: "button", onClick: () => setShowPreview((prev) => !prev), className: "border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: showPreview
|
|
272
465
|
? t("pages.reportProblem.form.hidePreview")
|
|
273
|
-
: t("pages.reportProblem.form.showPreview") }))] })] }), showPreview && currentDraft && (_jsxs("div", { className: "mt-4 bg-panel border border-line rounded-[6px] p-4", children: [_jsx("h3", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-3", children: "Markdown" }), _jsx("pre", { className: "whitespace-pre-wrap text-ink-2 text-[13px] leading-relaxed bg-surface border border-line rounded-[3px] p-3 max-h-[400px] overflow-auto", children: currentDraft.outputs.markdown })] }))] }), _jsx(PrivacySection, { draft: currentDraft }), currentDraft && (_jsxs("section", { className: "mt-8", "aria-labelledby": "section-current-draft", children: [_jsx(SectionTitle, { id: "section-current-draft", children: t("pages.reportProblem.draft.title") }), _jsx(DraftCard, { draft: currentDraft, onCopyMarkdown: () => handleCopyMarkdown(currentDraft), onCopyEmail: () => handleCopyEmail(currentDraft), onOpenGithub: () => handleOpenGithub(currentDraft), onOpenEmail: () => handleOpenEmail(currentDraft), onDelete: () => handleDeleteDraft(currentDraft.id) })] })), loadError && (_jsxs("div", { className: "mt-4 p-3 bg-panel border border-amber/20 rounded-[6px] text-ink-2 text-[13px]", children: [_jsx("p", { children: t("pages.reportProblem.loadError") }), _jsx("p", { className: "mt-1 text-ink-4 text-[12px] font-mono", children: loadError })] })), _jsx(SavedDraftsSection, { drafts: drafts, onLoad: handleLoadDraft, onDelete: handleDeleteDraft }), _jsx("footer", { className: "mt-12 pt-6 border-t border-line text-ink-3 text-[13px]", children: t("pages.reportProblem.privacy.guarantee") })] }) }));
|
|
466
|
+
: t("pages.reportProblem.form.showPreview") }))] })] }), showPreview && currentDraft && (_jsxs("div", { className: "mt-4 bg-panel border border-line rounded-[6px] p-4", children: [_jsx("h3", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-3", children: "Markdown" }), _jsx("pre", { className: "whitespace-pre-wrap text-ink-2 text-[13px] leading-relaxed bg-surface border border-line rounded-[3px] p-3 max-h-[400px] overflow-auto", children: currentDraft.outputs.markdown })] }))] }), _jsx(PrivacySection, { draft: currentDraft }), currentDraft && (_jsx(ChannelSubmitArea, { draft: currentDraft, channels: channels, onSubmitIngest: () => setConfirmCtx({ channel: "ingest" }), onSubmitGithub: () => setConfirmCtx({ channel: "github" }), onOpenEmail: () => handleOpenEmail(currentDraft), onExportFile: () => handleExportFile(currentDraft), onMarkSent: handleMarkSent })), currentDraft && (_jsxs("section", { className: "mt-8", "aria-labelledby": "section-current-draft", children: [_jsx(SectionTitle, { id: "section-current-draft", children: t("pages.reportProblem.draft.title") }), _jsx(DraftCard, { draft: currentDraft, onCopyMarkdown: () => handleCopyMarkdown(currentDraft), onCopyEmail: () => handleCopyEmail(currentDraft), onOpenGithub: () => handleOpenGithub(currentDraft), onOpenEmail: () => handleOpenEmail(currentDraft), onDelete: () => handleDeleteDraft(currentDraft.id) })] })), loadError && (_jsxs("div", { className: "mt-4 p-3 bg-panel border border-amber/20 rounded-[6px] text-ink-2 text-[13px]", children: [_jsx("p", { children: t("pages.reportProblem.loadError") }), _jsx("p", { className: "mt-1 text-ink-4 text-[12px] font-mono", children: loadError })] })), _jsx(SavedDraftsSection, { drafts: drafts, onLoad: handleLoadDraft, onDelete: handleDeleteDraft }), _jsx("footer", { className: "mt-12 pt-6 border-t border-line text-ink-3 text-[13px]", children: t("pages.reportProblem.privacy.guarantee") }), currentDraft && confirmCtx && (_jsx(ConfirmSubmitDialog, { draft: currentDraft, channel: confirmCtx.channel, open: true, busy: submittingChannel, onConfirm: () => handleSubmitChannel(confirmCtx.channel), onCancel: () => setConfirmCtx(null) }))] }) }));
|
|
274
467
|
}
|
|
@@ -50,6 +50,28 @@ export interface DeleteEnvelopeData {
|
|
|
50
50
|
deleted: boolean;
|
|
51
51
|
}
|
|
52
52
|
export declare function validateDeleteEnvelope(v: unknown): DeleteEnvelopeData | null;
|
|
53
|
+
export type FeedbackChannelId = 'ingest' | 'github' | 'email' | 'file';
|
|
54
|
+
export interface FeedbackChannelStatusData {
|
|
55
|
+
id: FeedbackChannelId;
|
|
56
|
+
available: boolean;
|
|
57
|
+
reason?: string;
|
|
58
|
+
nextAction?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface FeedbackChannelsData {
|
|
61
|
+
channels: FeedbackChannelStatusData[];
|
|
62
|
+
}
|
|
63
|
+
export declare function validateFeedbackChannels(v: unknown): FeedbackChannelsData | null;
|
|
64
|
+
export interface FeedbackSubmitResultData {
|
|
65
|
+
ok: boolean;
|
|
66
|
+
alreadySubmitted: boolean;
|
|
67
|
+
status: string;
|
|
68
|
+
submittedVia?: string;
|
|
69
|
+
trackingId?: string;
|
|
70
|
+
externalUrl?: string;
|
|
71
|
+
writeBackFailed?: boolean;
|
|
72
|
+
nextAction?: string;
|
|
73
|
+
}
|
|
74
|
+
export declare function validateFeedbackSubmitResult(v: unknown): FeedbackSubmitResultData | null;
|
|
53
75
|
interface WorkspaceConfigData {
|
|
54
76
|
workspaceName: string;
|
|
55
77
|
enabled: boolean;
|
|
@@ -139,6 +139,72 @@ export function validateDeleteEnvelope(v) {
|
|
|
139
139
|
return null;
|
|
140
140
|
return { deleted: v.deleted };
|
|
141
141
|
}
|
|
142
|
+
const CHANNEL_IDS = ['ingest', 'github', 'email', 'file'];
|
|
143
|
+
export function validateFeedbackChannels(v) {
|
|
144
|
+
if (!isObject(v))
|
|
145
|
+
return null;
|
|
146
|
+
if (!Object.hasOwn(v, 'channels') || !Array.isArray(v.channels))
|
|
147
|
+
return null;
|
|
148
|
+
const channels = [];
|
|
149
|
+
for (const item of v.channels) {
|
|
150
|
+
if (!isObject(item))
|
|
151
|
+
continue;
|
|
152
|
+
if (!Object.hasOwn(item, 'id') || !isString(item.id) || !CHANNEL_IDS.includes(item.id))
|
|
153
|
+
continue;
|
|
154
|
+
if (!Object.hasOwn(item, 'available') || !isBoolean(item.available))
|
|
155
|
+
continue;
|
|
156
|
+
const status = {
|
|
157
|
+
id: item.id,
|
|
158
|
+
available: item.available,
|
|
159
|
+
};
|
|
160
|
+
const reason = readNullableString(item, 'reason');
|
|
161
|
+
const nextAction = readNullableString(item, 'nextAction');
|
|
162
|
+
if (!reason.valid)
|
|
163
|
+
return null;
|
|
164
|
+
if (!nextAction.valid)
|
|
165
|
+
return null;
|
|
166
|
+
if (reason.value !== null)
|
|
167
|
+
status.reason = reason.value;
|
|
168
|
+
if (nextAction.value !== null)
|
|
169
|
+
status.nextAction = nextAction.value;
|
|
170
|
+
channels.push(status);
|
|
171
|
+
}
|
|
172
|
+
return { channels };
|
|
173
|
+
}
|
|
174
|
+
export function validateFeedbackSubmitResult(v) {
|
|
175
|
+
if (!isObject(v))
|
|
176
|
+
return null;
|
|
177
|
+
if (!Object.hasOwn(v, 'ok') || !isBoolean(v.ok))
|
|
178
|
+
return null;
|
|
179
|
+
if (!Object.hasOwn(v, 'alreadySubmitted') || !isBoolean(v.alreadySubmitted))
|
|
180
|
+
return null;
|
|
181
|
+
if (!Object.hasOwn(v, 'status') || !isString(v.status))
|
|
182
|
+
return null;
|
|
183
|
+
const result = {
|
|
184
|
+
ok: v.ok,
|
|
185
|
+
alreadySubmitted: v.alreadySubmitted,
|
|
186
|
+
status: v.status,
|
|
187
|
+
};
|
|
188
|
+
const submittedVia = readNullableString(v, 'submittedVia');
|
|
189
|
+
const trackingId = readNullableString(v, 'trackingId');
|
|
190
|
+
const externalUrl = readNullableString(v, 'externalUrl');
|
|
191
|
+
const nextAction = readNullableString(v, 'nextAction');
|
|
192
|
+
if (!submittedVia.valid || !trackingId.valid || !externalUrl.valid || !nextAction.valid)
|
|
193
|
+
return null;
|
|
194
|
+
if (submittedVia.value !== null)
|
|
195
|
+
result.submittedVia = submittedVia.value;
|
|
196
|
+
if (trackingId.value !== null)
|
|
197
|
+
result.trackingId = trackingId.value;
|
|
198
|
+
if (externalUrl.value !== null)
|
|
199
|
+
result.externalUrl = externalUrl.value;
|
|
200
|
+
if (nextAction.value !== null)
|
|
201
|
+
result.nextAction = nextAction.value;
|
|
202
|
+
if (Object.hasOwn(v, 'writeBackFailed') && !isBoolean(v.writeBackFailed))
|
|
203
|
+
return null;
|
|
204
|
+
if (isBoolean(v.writeBackFailed))
|
|
205
|
+
result.writeBackFailed = v.writeBackFailed;
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
142
208
|
function validateWorkspaceConfig(v) {
|
|
143
209
|
if (!isObject(v))
|
|
144
210
|
return null;
|