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.
Files changed (79) hide show
  1. package/console/dist/server/config/pd-config-store.d.ts +29 -0
  2. package/console/dist/server/config/pd-config-store.js +55 -0
  3. package/console/dist/server/feedback/channels.d.ts +36 -0
  4. package/console/dist/server/feedback/channels.js +89 -0
  5. package/console/dist/server/feedback/github-adapter.d.ts +44 -0
  6. package/console/dist/server/feedback/github-adapter.js +125 -0
  7. package/console/dist/server/feedback/ingest-adapter.d.ts +45 -0
  8. package/console/dist/server/feedback/ingest-adapter.js +129 -0
  9. package/console/dist/server/feedback/submit-service.d.ts +44 -0
  10. package/console/dist/server/feedback/submit-service.js +177 -0
  11. package/console/dist/server/index.js +23 -7
  12. package/console/dist/server/models/FeedbackReportConsoleModel.d.ts +8 -0
  13. package/console/dist/server/models/FeedbackReportConsoleModel.js +45 -0
  14. package/console/dist/server/routes/feedback-reports.d.ts +24 -0
  15. package/console/dist/server/routes/feedback-reports.js +151 -0
  16. package/console/dist/ui/api.d.ts +11 -3
  17. package/console/dist/ui/api.js +25 -2
  18. package/console/dist/ui/i18n/en.json +48 -2
  19. package/console/dist/ui/i18n/zh-CN.json +48 -2
  20. package/console/dist/ui/pages/ReportProblemValidators.d.ts +30 -0
  21. package/console/dist/ui/pages/ReportProblemValidators.js +66 -0
  22. package/console/dist/ui/pages/activation/ActivationPage.js +14 -3
  23. package/console/dist/ui/pages/pain/PainPage.js +6 -1
  24. package/console/dist/ui/pages/principles/PrincipleDetailPage.js +4 -1
  25. package/console/dist/ui/pages/report-problem/ReportProblemPage.js +263 -70
  26. package/console/dist/ui/utils/validators.d.ts +22 -0
  27. package/console/dist/ui/utils/validators.js +66 -0
  28. package/console/dist/web/assets/app.css +42 -0
  29. package/console/dist/web/assets/app.js +699 -127
  30. package/console/package.json +4 -4
  31. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +4 -0
  32. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
  33. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +1 -1
  34. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  35. package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.d.ts +2 -0
  36. package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.d.ts.map +1 -0
  37. package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.js +126 -0
  38. package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.js.map +1 -0
  39. package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.d.ts +2 -0
  40. package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.d.ts.map +1 -0
  41. package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.js +278 -0
  42. package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.js.map +1 -0
  43. package/core/dist/runtime-v2/feedback/create-report.d.ts.map +1 -1
  44. package/core/dist/runtime-v2/feedback/create-report.js +38 -1
  45. package/core/dist/runtime-v2/feedback/create-report.js.map +1 -1
  46. package/core/dist/runtime-v2/feedback/feedback-types.d.ts +60 -0
  47. package/core/dist/runtime-v2/feedback/feedback-types.d.ts.map +1 -1
  48. package/core/dist/runtime-v2/feedback/feedback-types.js +58 -0
  49. package/core/dist/runtime-v2/feedback/feedback-types.js.map +1 -1
  50. package/core/dist/runtime-v2/feedback/fingerprint.d.ts +31 -0
  51. package/core/dist/runtime-v2/feedback/fingerprint.d.ts.map +1 -0
  52. package/core/dist/runtime-v2/feedback/fingerprint.js +51 -0
  53. package/core/dist/runtime-v2/feedback/fingerprint.js.map +1 -0
  54. package/core/dist/runtime-v2/feedback/index.d.ts +3 -2
  55. package/core/dist/runtime-v2/feedback/index.d.ts.map +1 -1
  56. package/core/dist/runtime-v2/feedback/index.js +2 -1
  57. package/core/dist/runtime-v2/feedback/index.js.map +1 -1
  58. package/core/dist/runtime-v2/feedback/privacy-preview.d.ts.map +1 -1
  59. package/core/dist/runtime-v2/feedback/privacy-preview.js +43 -0
  60. package/core/dist/runtime-v2/feedback/privacy-preview.js.map +1 -1
  61. package/core/dist/runtime-v2/feedback/render-markdown.d.ts.map +1 -1
  62. package/core/dist/runtime-v2/feedback/render-markdown.js +33 -0
  63. package/core/dist/runtime-v2/feedback/render-markdown.js.map +1 -1
  64. package/core/dist/runtime-v2/index.d.ts +2 -2
  65. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  66. package/core/dist/runtime-v2/index.js +1 -1
  67. package/core/dist/runtime-v2/index.js.map +1 -1
  68. package/dist/installer.d.ts.map +1 -1
  69. package/dist/installer.js.map +1 -1
  70. package/dist/mvp-config.d.ts +22 -1
  71. package/dist/mvp-config.d.ts.map +1 -1
  72. package/dist/mvp-config.js +36 -1
  73. package/dist/mvp-config.js.map +1 -1
  74. package/package.json +1 -1
  75. package/plugin/dist/bundle.js +302 -302
  76. package/plugin/dist/openclaw.plugin.json +1 -18
  77. package/plugin/openclaw.plugin.json +2 -19
  78. package/plugin/package.json +3 -3
  79. 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 { createFeedbackReport, listFeedbackReports, getFeedbackReport, deleteFeedbackReport, fetchConfigSummary, request, } from "../../api.js";
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 VALID_SEVERITY_OPTIONS = new Set(SEVERITY_OPTIONS);
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 mb-2", children: t("pages.reportProblem.draft.savedAt", { id: draft.id, at: draft.createdAt }) }), _jsx("div", { className: "font-semibold text-ink mb-3", children: draft.title }), _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" })] }))] })] }));
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 mb-1", children: [enumLabel('feedbackType', draft.type, t), " \u00B7 ", draft.createdAt] }), _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))) }))] }));
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(INITIAL_FORM);
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
- // Task 6: read context identifiers (painId, principleId, approvalId,
80
- // activationId, taskId, source, page) from URL query params so the feedback
81
- // report can be associated with the originating entity.
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
- // ── Load drafts on mount ─────────────────────────────────────────────────
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
- }, [loadDrafts]);
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(input, diagnostics);
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
- // Validate response with parseEnvelopeReport (ERR-001/005/009)
150
- const parsed = parseEnvelopeReport(result.data);
151
- if (parsed === null) {
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
- else {
169
- setCurrentDraft(parsed);
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
- // Reset form
173
- setForm(INITIAL_FORM);
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, contextFromUrl]);
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
- handleFieldChange("type", val);
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: [_jsx("option", { value: "", children: t("pages.reportProblem.form.severityNotSet") }), SEVERITY_OPTIONS.map((sev) => (_jsx("option", { value: sev, children: t(`pages.reportProblem.form.severity${sev.charAt(0).toUpperCase() + sev.slice(1)}`) }, sev)))] })] }), _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" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "feedback-steps", className: "block text-ink-2 text-[13px] mb-1.5", children: t("pages.reportProblem.form.stepsLabel") }), _jsx("textarea", { id: "feedback-steps", value: form.stepsToReproduce, onChange: (e) => handleFieldChange("stepsToReproduce", e.target.value), rows: 3, 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" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "feedback-expected", className: "block text-ink-2 text-[13px] mb-1.5", children: t("pages.reportProblem.form.expectedLabel") }), _jsx("textarea", { id: "feedback-expected", value: form.expectedBehavior, onChange: (e) => handleFieldChange("expectedBehavior", e.target.value), rows: 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" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "feedback-actual", className: "block text-ink-2 text-[13px] mb-1.5", children: t("pages.reportProblem.form.actualLabel") }), _jsx("textarea", { id: "feedback-actual", value: form.actualBehavior, onChange: (e) => handleFieldChange("actualBehavior", e.target.value), rows: 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" })] }), _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
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;