Unverified 提交 b53dd77a authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Improve Help dialog UX: DRY refactor, animations, and visual hierarchy (#2629)

- Extract shared helpers (formatSettingsLines, formatSystemInfoSection, openGitHubIssue) to eliminate duplication between Report a Bug and Upload Chat Session flows - Extract reusable components (AnimatedScreen, ReviewDetailsSection, CopyButton) for modularity - Add smooth slide animations between dialog screens using framer-motion, with no animation on initial open - Collapse all review screen sections into accordions - Redesign upload complete screen: inline checkmark, animated copy button, Create GitHub Issue button above warning banner - Include error logs and system info in Upload Chat Session's GitHub issue (previously only in Report a Bug) - Add visual hierarchy to main screen: bordered cards for each report option, labeled divider separating self-help from issue reporting - Show clear "Open a chat first" warning when no chat is selected <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2629" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Primarily UI/UX refactoring and animation changes, plus expanded GitHub issue prefill content; low risk but could affect support/reporting flow behavior if screen navigation or issue URL construction regresses. > > **Overview** > Refactors `HelpDialog` into a 3-screen flow (`main` → `review` → `upload-complete`) with slide transitions via `framer-motion`, including reusable subcomponents (`AnimatedScreen`, `ReviewDetailsSection`, `CopyButton`) and a navigation helper. > > Unifies GitHub issue creation for both bug reports and session uploads with shared formatting helpers and `openGitHubIssue`, and **expands session-upload issues to include system info, settings, and recent logs** (plus a `v2:` session ID prefix). Updates the main screen layout into clearer “self-help” vs “report an issue” sections with card styling and a disabled-state warning when no chat is selected, and makes review sections collapsible plus redesigns the upload-complete screen with an animated copy affordance and a clearer call-to-action to create the GitHub issue. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a3b76962ec8796ce1a081dcfb8447d5da4d2e15b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: 's avatarCursor <cursoragent@cursor.com>
上级 9b2146f3
......@@ -4,7 +4,6 @@ import {
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import {
......@@ -14,19 +13,231 @@ import {
ChevronLeftIcon,
CheckIcon,
XIcon,
FileIcon,
SparklesIcon,
ExternalLinkIcon,
AlertCircleIcon,
MessageSquareIcon,
CopyIcon,
} from "lucide-react";
import { ipc } from "@/ipc/types";
import { useState, useEffect } from "react";
import {
type ReactNode,
useState,
useEffect,
useRef,
useCallback,
} from "react";
import { useAtomValue } from "jotai";
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { SessionDebugBundle } from "@/ipc/types";
import { type SessionDebugBundle, type SystemDebugInfo } from "@/ipc/types";
import { showError } from "@/lib/toast";
import { HelpBotDialog } from "./HelpBotDialog";
import { useSettings } from "@/hooks/useSettings";
import { BugScreenshotDialog } from "./BugScreenshotDialog";
import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo";
import { type UserSettings } from "@/lib/schemas";
import { type UserBudgetInfo } from "@/ipc/types/system";
import { motion, AnimatePresence } from "framer-motion";
// =============================================================================
// Animation constants
// =============================================================================
type DialogScreen = "main" | "review" | "upload-complete";
const SCREEN_ORDER: DialogScreen[] = ["main", "review", "upload-complete"];
const screenVariants = {
enter: (direction: number) => ({
x: direction > 0 ? 80 : -80,
opacity: 0,
}),
center: { x: 0, opacity: 1 },
exit: (direction: number) => ({
x: direction < 0 ? 80 : -80,
opacity: 0,
}),
};
const screenTransition = {
x: { type: "spring" as const, stiffness: 400, damping: 35 },
opacity: { duration: 0.15 },
};
// =============================================================================
// GitHub issue helpers (shared between Report a Bug & Upload Chat Session)
// =============================================================================
const GITHUB_ISSUES_BASE =
"https://github.com/dyad-sh/dyad/issues/new" as const;
function formatSettingsLines(settings: UserSettings | null): string {
if (!settings) return "Settings not available";
return [
`- Selected Model: ${settings.selectedModel?.provider}:${settings.selectedModel?.name}`,
`- Chat Mode: ${settings.selectedChatMode ?? "default"}`,
`- Auto Approve Changes: ${settings.autoApproveChanges ?? "n/a"}`,
`- Dyad Pro Enabled: ${settings.enableDyadPro ?? "n/a"}`,
`- Thinking Budget: ${settings.thinkingBudget ?? "n/a"}`,
`- Runtime Mode: ${settings.runtimeMode2 ?? "n/a"}`,
`- Release Channel: ${settings.releaseChannel ?? "n/a"}`,
`- Auto Fix Problems: ${settings.enableAutoFixProblems ?? "n/a"}`,
`- Native Git: ${settings.enableNativeGit ?? "n/a"}`,
].join("\n");
}
function formatSystemInfoSection(
debugInfo: SystemDebugInfo,
userBudget: UserBudgetInfo | undefined,
): string {
return `## System Information
- Dyad Version: ${debugInfo.dyadVersion}
- Platform: ${debugInfo.platform}
- Architecture: ${debugInfo.architecture}
- Node Version: ${debugInfo.nodeVersion || "n/a"}
- PNPM Version: ${debugInfo.pnpmVersion || "n/a"}
- Node Path: ${debugInfo.nodePath || "n/a"}
- Pro User ID: ${userBudget?.redactedUserId || "n/a"}
- Telemetry ID: ${debugInfo.telemetryId || "n/a"}
- Model: ${debugInfo.selectedLanguageModel || "n/a"}`;
}
function formatLogsSection(debugInfo: SystemDebugInfo): string {
return `## Logs
\`\`\`
${debugInfo.logs.slice(-3_500) || "No logs available"}
\`\`\``;
}
function openGitHubIssue(params: {
title: string;
labels: string[];
body: string;
isDyadProUser: unknown;
}) {
const labels = [...params.labels];
if (params.isDyadProUser) labels.push("pro");
const qs = new URLSearchParams({
title: params.title,
labels: labels.join(","),
body: params.body,
});
ipc.system.openExternalUrl(`${GITHUB_ISSUES_BASE}?${qs.toString()}`);
}
// =============================================================================
// Reusable sub-components
// =============================================================================
/** Animated wrapper applied to every dialog screen. */
function AnimatedScreen({
screenKey,
direction,
skipInitial,
className,
children,
}: {
screenKey: string;
direction: number;
skipInitial?: boolean;
className?: string;
children: ReactNode;
}) {
return (
<motion.div
key={screenKey}
custom={direction}
variants={screenVariants}
initial={skipInitial ? false : "enter"}
animate="center"
exit="exit"
transition={screenTransition}
className={className}
>
{children}
</motion.div>
);
}
/** A collapsible section in the review screen. */
function ReviewDetailsSection({
title,
children,
mono,
data,
}: {
title: string;
children?: ReactNode;
mono?: boolean;
data?: unknown;
}) {
return (
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">{title}</summary>
<div
className={`text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 ${mono !== false ? "font-mono" : ""} whitespace-pre-wrap`}
>
{data !== undefined ? JSON.stringify(data, null, 2) : children}
</div>
</details>
);
}
/** Copy button with animated feedback. */
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
} catch (err) {
console.error("Failed to copy:", err);
}
}, [text]);
useEffect(() => {
if (!copied) return;
const timer = setTimeout(() => setCopied(false), 2000);
return () => clearTimeout(timer);
}, [copied]);
return (
<button
onClick={handleCopy}
className="shrink-0 p-1.5 rounded-md hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors"
aria-label="Copy session ID"
>
<AnimatePresence mode="wait" initial={false}>
{copied ? (
<motion.div
key="check"
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.5, opacity: 0 }}
transition={{ duration: 0.15 }}
>
<CheckIcon className="h-3.5 w-3.5 text-green-600 dark:text-green-400" />
</motion.div>
) : (
<motion.div
key="copy"
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.5, opacity: 0 }}
transition={{ duration: 0.15 }}
>
<CopyIcon className="h-3.5 w-3.5 text-muted-foreground" />
</motion.div>
)}
</AnimatePresence>
</button>
);
}
// =============================================================================
// Main component
// =============================================================================
interface HelpDialogProps {
isOpen: boolean;
......@@ -36,63 +247,57 @@ interface HelpDialogProps {
export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
const [isLoading, setIsLoading] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [reviewMode, setReviewMode] = useState(false);
const [screen, setScreen] = useState<DialogScreen>("main");
const [direction, setDirection] = useState(0);
const [debugBundle, setDebugBundle] = useState<SessionDebugBundle | null>(
null,
);
const [uploadComplete, setUploadComplete] = useState(false);
const [sessionId, setSessionId] = useState("");
const [isHelpBotOpen, setIsHelpBotOpen] = useState(false);
const [isBugScreenshotOpen, setIsBugScreenshotOpen] = useState(false);
const hasNavigated = useRef(false);
const selectedChatId = useAtomValue(selectedChatIdAtom);
const { settings } = useSettings();
const { userBudget } = useUserBudgetInfo();
const isDyadProUser = settings?.providerSettings?.["auto"]?.apiKey?.value;
// Function to reset all dialog state
// ---------------------------------------------------------------------------
// Navigation
// ---------------------------------------------------------------------------
const navigateTo = (newScreen: DialogScreen) => {
const currentIdx = SCREEN_ORDER.indexOf(screen);
const newIdx = SCREEN_ORDER.indexOf(newScreen);
setDirection(newIdx > currentIdx ? 1 : -1);
setScreen(newScreen);
hasNavigated.current = true;
};
const resetDialogState = () => {
setIsLoading(false);
setIsUploading(false);
setReviewMode(false);
setScreen("main");
setDirection(0);
setDebugBundle(null);
setUploadComplete(false);
setSessionId("");
hasNavigated.current = false;
};
// Reset state when dialog closes or reopens
useEffect(() => {
if (!isOpen) {
resetDialogState();
}
if (!isOpen) resetDialogState();
}, [isOpen]);
// Wrap the original onClose to also reset state
const handleClose = () => {
onClose();
};
const handleClose = () => onClose();
// ---------------------------------------------------------------------------
// Actions
// ---------------------------------------------------------------------------
const handleReportBug = async () => {
setIsLoading(true);
try {
// Get system debug info
const debugInfo = await ipc.system.getSystemDebugInfo();
// Create a formatted issue body with the debug info
const settingsLines = settings
? [
`- Selected Model: ${settings.selectedModel?.provider}:${settings.selectedModel?.name}`,
`- Chat Mode: ${settings.selectedChatMode ?? "default"}`,
`- Auto Approve Changes: ${settings.autoApproveChanges ?? "n/a"}`,
`- Dyad Pro Enabled: ${settings.enableDyadPro ?? "n/a"}`,
`- Thinking Budget: ${settings.thinkingBudget ?? "n/a"}`,
`- Runtime Mode: ${settings.runtimeMode2 ?? "n/a"}`,
`- Release Channel: ${settings.releaseChannel ?? "n/a"}`,
`- Auto Fix Problems: ${settings.enableAutoFixProblems ?? "n/a"}`,
`- Native Git: ${settings.enableNativeGit ?? "n/a"}`,
].join("\n")
: "Settings not available";
const issueBody = `
const body = `\
<!-- Please fill in all fields in English -->
## Bug Description (required)
......@@ -101,41 +306,22 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
## Screenshot (recommended)
<!-- Screenshot of the bug -->
## System Information
- Dyad Version: ${debugInfo.dyadVersion}
- Platform: ${debugInfo.platform}
- Architecture: ${debugInfo.architecture}
- Node Version: ${debugInfo.nodeVersion || "n/a"}
- PNPM Version: ${debugInfo.pnpmVersion || "n/a"}
- Node Path: ${debugInfo.nodePath || "n/a"}
- Pro User ID: ${userBudget?.redactedUserId || "n/a"}
- Telemetry ID: ${debugInfo.telemetryId || "n/a"}
- Model: ${debugInfo.selectedLanguageModel || "n/a"}
${formatSystemInfoSection(debugInfo, userBudget ?? undefined)}
## Settings
${settingsLines}
${formatSettingsLines(settings)}
## Logs
\`\`\`
${debugInfo.logs.slice(-3_500) || "No logs available"}
\`\`\`
${formatLogsSection(debugInfo)}
`;
// Create the GitHub issue URL with the pre-filled body
const encodedBody = encodeURIComponent(issueBody);
const encodedTitle = encodeURIComponent("[bug] <WRITE TITLE HERE>");
const labels = ["bug"];
if (isDyadProUser) {
labels.push("pro");
}
const githubIssueUrl = `https://github.com/dyad-sh/dyad/issues/new?title=${encodedTitle}&labels=${labels}&body=${encodedBody}`;
// Open the pre-filled GitHub issue page
ipc.system.openExternalUrl(githubIssueUrl);
openGitHubIssue({
title: "[bug] <WRITE TITLE HERE>",
labels: ["bug"],
body,
isDyadProUser,
});
} catch (error) {
console.error("Failed to prepare bug report:", error);
// Fallback to opening the regular GitHub issue page
ipc.system.openExternalUrl("https://github.com/dyad-sh/dyad/issues/new");
ipc.system.openExternalUrl(GITHUB_ISSUES_BASE);
} finally {
setIsLoading(false);
}
......@@ -146,15 +332,11 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
alert("Please select a chat first");
return;
}
setIsUploading(true);
try {
// Get chat logs (includes debug info, chat data, and codebase)
const debugBundle = await ipc.misc.getSessionDebugBundle(selectedChatId);
// Store data for review and switch to review mode
setDebugBundle(debugBundle);
setReviewMode(true);
const bundle = await ipc.misc.getSessionDebugBundle(selectedChatId);
setDebugBundle(bundle);
navigateTo("review");
} catch (error) {
console.error("Failed to upload chat session:", error);
alert(
......@@ -167,43 +349,31 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
const handleSubmitChatLogs = async () => {
if (!debugBundle) return;
setIsUploading(true);
try {
// Get signed URL
const response = await fetch(
"https://upload-logs.dyad.sh/generate-upload-url",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
extension: "json",
contentType: "application/json",
}),
},
);
if (!response.ok) {
showError(`Failed to get upload URL: ${response.statusText}`);
throw new Error(`Failed to get upload URL: ${response.statusText}`);
}
const { uploadUrl, filename } = await response.json();
// Upload the full debug bundle directly
await ipc.system.uploadToSignedUrl({
url: uploadUrl,
contentType: "application/json",
data: debugBundle,
});
// Extract session ID (filename without extension)
const sessionId = filename.replace(".json", "");
setSessionId(sessionId);
setUploadComplete(true);
setReviewMode(false);
setSessionId("v2:" + filename.replace(".json", ""));
navigateTo("upload-complete");
} catch (error) {
console.error("Failed to upload chat logs:", error);
alert("Failed to upload chat logs. Please try again.");
......@@ -213,13 +383,14 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
};
const handleCancelReview = () => {
setReviewMode(false);
navigateTo("main");
setDebugBundle(null);
};
const handleOpenGitHubIssue = () => {
// Create a GitHub issue with the session ID
const issueBody = `
const handleOpenGitHubIssue = async () => {
try {
const debugInfo = await ipc.system.getSystemDebugInfo();
const body = `\
<!-- Please fill in all fields in English -->
Session ID: ${sessionId}
......@@ -234,233 +405,121 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"}
## Actual Behavior (required)
<!-- What actually happened? -->
`;
const encodedBody = encodeURIComponent(issueBody);
const encodedTitle = encodeURIComponent("[session report] <add title>");
const labels = ["support"];
if (isDyadProUser) {
labels.push("pro");
}
const githubIssueUrl = `https://github.com/dyad-sh/dyad/issues/new?title=${encodedTitle}&labels=${labels}&body=${encodedBody}`;
${formatSystemInfoSection(debugInfo, userBudget ?? undefined)}
ipc.system.openExternalUrl(githubIssueUrl);
## Settings
${formatSettingsLines(settings)}
${formatLogsSection(debugInfo)}
`;
openGitHubIssue({
title: "[session report] <add title>",
labels: ["support"],
body,
isDyadProUser,
});
} catch (error) {
console.error("Failed to prepare session report:", error);
openGitHubIssue({
title: "[session report] <add title>",
labels: ["support"],
body: `Session ID: ${sessionId}\nSession Schema: v2.0\nPro User ID: ${userBudget?.redactedUserId || "n/a"}`,
isDyadProUser,
});
}
handleClose();
};
if (uploadComplete) {
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Upload Complete</DialogTitle>
</DialogHeader>
<div className="py-6 flex flex-col items-center space-y-4">
<div className="bg-green-50 dark:bg-green-900/20 p-6 rounded-full">
<CheckIcon className="h-8 w-8 text-green-600 dark:text-green-400" />
</div>
<h3 className="text-lg font-medium">
Chat Logs Uploaded Successfully
</h3>
<div className="bg-slate-100 dark:bg-slate-800 p-3 rounded flex items-center space-x-2 font-mono text-sm">
<FileIcon
className="h-4 w-4 cursor-pointer"
onClick={async () => {
try {
await navigator.clipboard.writeText(sessionId);
} catch (err) {
console.error("Failed to copy session ID:", err);
}
}}
/>
<span>{sessionId}</span>
</div>
<p className="text-center text-sm">
You must open a GitHub issue for us to investigate. Without a
linked issue, your report will not be reviewed.
</p>
</div>
<DialogFooter>
<Button onClick={handleOpenGitHubIssue} className="w-full">
Open GitHub Issue
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
if (reviewMode && debugBundle) {
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center">
<Button
variant="ghost"
className="mr-2 p-0 h-8 w-8"
onClick={handleCancelReview}
>
<ChevronLeftIcon className="h-4 w-4" />
</Button>
OK to upload chat session?
</DialogTitle>
</DialogHeader>
<DialogDescription>
Please review the information that will be submitted. Your chat
messages, system information, and a snapshot of your codebase will
be included.
</DialogDescription>
<div className="space-y-4 overflow-y-auto flex-grow">
<div className="border rounded-md p-3">
<h3 className="font-medium mb-2">Chat Messages</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto">
{debugBundle.chat.messages.map((msg) => (
<div key={msg.id} className="mb-2">
<span className="font-semibold">
{msg.role === "user" ? "You" : "Assistant"}:{" "}
</span>
<span>{msg.content}</span>
</div>
))}
</div>
</div>
<div className="border rounded-md p-3">
<h3 className="font-medium mb-2">Codebase Snapshot</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto font-mono">
{debugBundle.codebase}
</div>
</div>
<div className="border rounded-md p-3">
<h3 className="font-medium mb-2">Logs</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto font-mono">
{debugBundle.logs}
</div>
</div>
// ---------------------------------------------------------------------------
// Screens
// ---------------------------------------------------------------------------
const renderMainScreen = () => (
<AnimatedScreen
screenKey="main"
direction={direction}
skipInitial={!hasNavigated.current}
>
<DialogHeader>
<DialogTitle>Need help with Dyad?</DialogTitle>
</DialogHeader>
<DialogDescription>
If you need help or want to report an issue, here are some options:
</DialogDescription>
<div className="flex flex-col w-full mt-4 space-y-5">
{/* Self-service help */}
{isDyadProUser ? (
<Button
variant="default"
onClick={() => setIsHelpBotOpen(true)}
className="w-full py-6 border-primary/50 shadow-sm shadow-primary/10 transition-all hover:shadow-md hover:shadow-primary/15"
>
<SparklesIcon className="mr-2 h-5 w-5" /> Chat with Dyad help bot
(Pro)
</Button>
) : (
<Button
variant="outline"
onClick={() =>
ipc.system.openExternalUrl("https://www.dyad.sh/docs")
}
className="w-full py-6 bg-(--background-lightest)"
>
<BookOpenIcon className="mr-2 h-5 w-5" /> Open Docs
</Button>
)}
{/* Divider */}
<div className="flex items-center gap-3">
<div className="h-px flex-1 bg-border" />
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Report an issue
</span>
<div className="h-px flex-1 bg-border" />
</div>
<div className="border rounded-md p-3">
<h3 className="font-medium mb-2">System Information</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-32 overflow-y-auto">
<p>Dyad Version: {debugBundle.system.dyadVersion}</p>
<p>Platform: {debugBundle.system.platform}</p>
<p>Architecture: {debugBundle.system.architecture}</p>
<p>
Node Version:{" "}
{debugBundle.system.nodeVersion || "Not available"}
</p>
</div>
{/* Report options */}
<div className="grid grid-cols-1 gap-3">
{/* Upload Chat Session */}
<div className="border rounded-lg p-4 space-y-3 relative">
<div className="flex items-center gap-2">
<MessageSquareIcon className="h-4 w-4 text-primary" />
<span className="text-sm font-semibold">
AI / Dyad Pro issues
</span>
</div>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">Settings</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.settings, null, 2)}
</div>
</details>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">
App Metadata
</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.app, null, 2)}
</div>
</details>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">
Custom Providers & Models
</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.providers, null, 2)}
</div>
</details>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">
MCP Servers
</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.mcpServers, null, 2)}
</div>
</details>
</div>
<div className="flex justify-between mt-4 pt-2 sticky bottom-0 bg-background">
<p className="text-sm text-muted-foreground">
Best for AI quality issues. Uploads your chat session and code for
the team to reproduce and fix the problem.
</p>
<Button
variant="outline"
onClick={handleCancelReview}
className="flex items-center"
>
<XIcon className="mr-2 h-4 w-4" /> Cancel
</Button>
<Button
onClick={handleSubmitChatLogs}
className="flex items-center"
disabled={isUploading}
onClick={handleUploadChatSession}
disabled={isUploading || !selectedChatId}
className="w-full bg-(--background-lightest)"
>
{isUploading ? (
"Uploading..."
) : (
<>
<CheckIcon className="mr-2 h-4 w-4" /> Upload
</>
)}
<UploadIcon className="mr-2 h-4 w-4" />{" "}
{isUploading ? "Preparing Upload..." : "Upload Chat Session"}
</Button>
{!selectedChatId && (
<p className="text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1.5">
<AlertCircleIcon className="h-3 w-3 shrink-0" />
Open a chat first to upload a session.
</p>
)}
</div>
</DialogContent>
</Dialog>
);
}
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Need help with Dyad?</DialogTitle>
</DialogHeader>
<DialogDescription className="">
If you need help or want to report an issue, here are some options:
</DialogDescription>
<div className="flex flex-col space-y-4 w-full">
{isDyadProUser ? (
<div className="flex flex-col space-y-2">
<Button
variant="default"
onClick={() => {
setIsHelpBotOpen(true);
}}
className="w-full py-6 border-primary/50 shadow-sm shadow-primary/10 transition-all hover:shadow-md hover:shadow-primary/15"
>
<SparklesIcon className="mr-2 h-5 w-5" /> Chat with Dyad help
bot (Pro)
</Button>
<p className="text-sm text-muted-foreground px-2">
Opens an in-app help chat assistant that searches through Dyad's
docs.
</p>
{/* Report a Bug */}
<div className="border rounded-lg p-4 space-y-3">
<div className="flex items-center gap-2">
<BugIcon className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-semibold">Non-AI issues</span>
</div>
) : (
<div className="flex flex-col space-y-2">
<Button
variant="outline"
onClick={() => {
ipc.system.openExternalUrl("https://www.dyad.sh/docs");
}}
className="w-full py-6 bg-(--background-lightest)"
>
<BookOpenIcon className="mr-2 h-5 w-5" /> Open Docs
</Button>
<p className="text-sm text-muted-foreground px-2">
Get help with common questions and issues.
</p>
</div>
)}
<div className="flex flex-col space-y-2">
<p className="text-sm text-muted-foreground">
Includes error logs to troubleshoot non-AI issues with Dyad (UI
bugs, crashes, setup problems, etc.).
</p>
<Button
variant="outline"
onClick={() => {
......@@ -468,33 +527,166 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"}
setIsBugScreenshotOpen(true);
}}
disabled={isLoading}
className="w-full py-6 bg-(--background-lightest)"
className="w-full bg-(--background-lightest)"
>
<BugIcon className="mr-2 h-5 w-5" />{" "}
<BugIcon className="mr-2 h-4 w-4" />{" "}
{isLoading ? "Preparing Report..." : "Report a Bug"}
</Button>
<p className="text-sm text-muted-foreground px-2">
We'll auto-fill your report with system info and logs. You can
review it for any sensitive info before submitting.
</p>
</div>
<div className="flex flex-col space-y-2">
</div>
</div>
</AnimatedScreen>
);
const renderReviewScreen = () =>
debugBundle && (
<AnimatedScreen
screenKey="review"
direction={direction}
className="flex flex-col overflow-hidden"
>
<DialogHeader>
<DialogTitle className="flex items-center">
<Button
variant="outline"
onClick={handleUploadChatSession}
disabled={isUploading || !selectedChatId}
className="w-full py-6 bg-(--background-lightest)"
variant="ghost"
className="mr-2 p-0 h-8 w-8"
onClick={handleCancelReview}
>
<UploadIcon className="mr-2 h-5 w-5" />{" "}
{isUploading ? "Preparing Upload..." : "Upload Chat Session"}
<ChevronLeftIcon className="h-4 w-4" />
</Button>
<p className="text-sm text-muted-foreground px-2">
Share chat logs and code for troubleshooting. Data is used only to
resolve your issue and auto-deleted after a limited time.
OK to upload chat session?
</DialogTitle>
</DialogHeader>
<DialogDescription>
Please review the information that will be submitted. Your chat
messages, system information, and a snapshot of your codebase will be
included.
</DialogDescription>
<div className="space-y-2 overflow-y-auto flex-grow mt-4">
<ReviewDetailsSection title="Chat Messages" mono={false}>
{debugBundle.chat.messages.map((msg) => (
<div key={msg.id} className="mb-2">
<span className="font-semibold">
{msg.role === "user" ? "You" : "Assistant"}:{" "}
</span>
<span>{msg.content}</span>
</div>
))}
</ReviewDetailsSection>
<ReviewDetailsSection title="Codebase Snapshot">
{debugBundle.codebase}
</ReviewDetailsSection>
<ReviewDetailsSection title="Logs">
{debugBundle.logs}
</ReviewDetailsSection>
<ReviewDetailsSection title="System Information" mono={false}>
<p>Dyad Version: {debugBundle.system.dyadVersion}</p>
<p>Platform: {debugBundle.system.platform}</p>
<p>Architecture: {debugBundle.system.architecture}</p>
<p>
Node Version: {debugBundle.system.nodeVersion || "Not available"}
</p>
</div>
</ReviewDetailsSection>
<ReviewDetailsSection title="Settings" data={debugBundle.settings} />
<ReviewDetailsSection title="App Metadata" data={debugBundle.app} />
<ReviewDetailsSection
title="Custom Providers & Models"
data={debugBundle.providers}
/>
<ReviewDetailsSection
title="MCP Servers"
data={debugBundle.mcpServers}
/>
</div>
</DialogContent>
<div className="flex justify-between mt-4 pt-2 sticky bottom-0 bg-background">
<Button
variant="outline"
onClick={handleCancelReview}
className="flex items-center"
>
<XIcon className="mr-2 h-4 w-4" /> Cancel
</Button>
<Button
onClick={handleSubmitChatLogs}
className="flex items-center"
disabled={isUploading}
>
{isUploading ? (
"Uploading..."
) : (
<>
<CheckIcon className="mr-2 h-4 w-4" /> Upload
</>
)}
</Button>
</div>
</AnimatedScreen>
);
const renderUploadCompleteScreen = () => (
<AnimatedScreen screenKey="upload-complete" direction={direction}>
<DialogHeader>
<DialogTitle>Upload Complete</DialogTitle>
</DialogHeader>
<div className="flex items-center gap-2.5 mt-3">
<CheckIcon className="h-5 w-5 text-green-600 dark:text-green-400 shrink-0" />
<span className="text-base font-medium">Chat session uploaded</span>
</div>
<div className="bg-slate-100 dark:bg-slate-800 px-3 py-2 rounded-md flex items-center gap-2 font-mono text-sm mt-2">
<span className="truncate flex-1 select-all">{sessionId}</span>
<CopyButton text={sessionId} />
</div>
<Button
onClick={handleOpenGitHubIssue}
className="w-full py-5 text-base mt-4"
size="lg"
>
<ExternalLinkIcon className="mr-2 h-5 w-5" />
Create GitHub Issue
</Button>
<div className="border border-amber-300 dark:border-amber-600 bg-amber-50 dark:bg-amber-950/30 rounded-lg p-3 mt-3">
<div className="flex items-start gap-2">
<AlertCircleIcon className="h-4 w-4 text-amber-600 dark:text-amber-400 shrink-0 mt-0.5" />
<p className="text-sm text-amber-700 dark:text-amber-400/80">
Your upload will not be reviewed without a linked GitHub issue. The
issue will be pre-filled with your session ID and system info.
</p>
</div>
</div>
</AnimatedScreen>
);
// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
return (
<>
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent
className={
screen === "review"
? "max-w-4xl max-h-[80vh] overflow-hidden flex flex-col"
: undefined
}
>
<AnimatePresence mode="wait" custom={direction}>
{screen === "main" && renderMainScreen()}
{screen === "review" && renderReviewScreen()}
{screen === "upload-complete" && renderUploadCompleteScreen()}
</AnimatePresence>
</DialogContent>
</Dialog>
<HelpBotDialog
isOpen={isHelpBotOpen}
onClose={() => setIsHelpBotOpen(false)}
......@@ -505,6 +697,6 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"}
handleReportBug={handleReportBug}
isLoading={isLoading}
/>
</Dialog>
</>
);
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论