Click to edit UI (#385)
- [x] add e2e test - happy case (make sure it clears selection and next prompt is empty, and preview is cleared); de-selection case - [x] shim - old & new file - [x] upgrade path - [x] add docs - [x] add try-catch to parser script - [x] make it work for next.js - [x] extract npm package - [x] make sure plugin doesn't apply in prod
This commit is contained in:
152
src/components/AppUpgrades.tsx
Normal file
152
src/components/AppUpgrades.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Terminal } from "lucide-react";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AppUpgrade } from "@/ipc/ipc_types";
|
||||
|
||||
export function AppUpgrades({ appId }: { appId: number | null }) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
data: upgrades,
|
||||
isLoading,
|
||||
error: queryError,
|
||||
} = useQuery({
|
||||
queryKey: ["app-upgrades", appId],
|
||||
queryFn: () => {
|
||||
if (!appId) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return IpcClient.getInstance().getAppUpgrades({ appId });
|
||||
},
|
||||
enabled: !!appId,
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: executeUpgrade,
|
||||
isPending: isUpgrading,
|
||||
error: mutationError,
|
||||
variables: upgradingVariables,
|
||||
} = useMutation({
|
||||
mutationFn: (upgradeId: string) => {
|
||||
if (!appId) {
|
||||
throw new Error("appId is not set");
|
||||
}
|
||||
return IpcClient.getInstance().executeAppUpgrade({
|
||||
appId,
|
||||
upgradeId,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["app-upgrades", appId] });
|
||||
},
|
||||
});
|
||||
|
||||
const handleUpgrade = (upgradeId: string) => {
|
||||
executeUpgrade(upgradeId);
|
||||
};
|
||||
|
||||
if (!appId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">
|
||||
App Upgrades
|
||||
</h3>
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (queryError) {
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">
|
||||
App Upgrades
|
||||
</h3>
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Error loading upgrades</AlertTitle>
|
||||
<AlertDescription>{queryError.message}</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const currentUpgrades = upgrades?.filter((u) => u.isNeeded) ?? [];
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">
|
||||
App Upgrades
|
||||
</h3>
|
||||
{currentUpgrades.length === 0 ? (
|
||||
<div
|
||||
data-testid="no-app-upgrades-needed"
|
||||
className="p-4 bg-green-50 border border-green-200 dark:bg-green-900/20 dark:border-green-800/50 rounded-lg text-sm text-green-800 dark:text-green-300"
|
||||
>
|
||||
App is up-to-date and has all Dyad capabilities enabled
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{currentUpgrades.map((upgrade: AppUpgrade) => (
|
||||
<div
|
||||
key={upgrade.id}
|
||||
className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg flex justify-between items-start"
|
||||
>
|
||||
<div className="flex-grow">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200">
|
||||
{upgrade.title}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{upgrade.description}
|
||||
</p>
|
||||
{mutationError && upgradingVariables === upgrade.id && (
|
||||
<Alert
|
||||
variant="destructive"
|
||||
className="mt-3 dark:bg-destructive/15"
|
||||
>
|
||||
<Terminal className="h-4 w-4" />
|
||||
<AlertTitle className="dark:text-red-200">
|
||||
Upgrade Failed
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-xs text-red-400 dark:text-red-300">
|
||||
{(mutationError as Error).message}{" "}
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
IpcClient.getInstance().openExternalUrl(
|
||||
upgrade.manualUpgradeUrl ?? "https://dyad.sh/docs",
|
||||
);
|
||||
}}
|
||||
className="underline font-medium hover:dark:text-red-200"
|
||||
>
|
||||
Manual Upgrade Instructions
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleUpgrade(upgrade.id)}
|
||||
disabled={isUpgrading && upgradingVariables === upgrade.id}
|
||||
className="ml-4 flex-shrink-0"
|
||||
size="sm"
|
||||
data-testid={`app-upgrade-${upgrade.id}`}
|
||||
>
|
||||
{isUpgrading && upgradingVariables === upgrade.id ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -191,6 +191,7 @@ export function ChatHeader({
|
||||
</div>
|
||||
|
||||
<button
|
||||
data-testid="toggle-preview-panel-button"
|
||||
onClick={onTogglePreview}
|
||||
className="cursor-pointer p-2 hover:bg-(--background-lightest) rounded-md"
|
||||
>
|
||||
|
||||
@@ -61,6 +61,9 @@ import { DragDropOverlay } from "./DragDropOverlay";
|
||||
import { showError, showExtraFilesToast } from "@/lib/toast";
|
||||
import { ChatInputControls } from "../ChatInputControls";
|
||||
import { ChatErrorBox } from "./ChatErrorBox";
|
||||
import { selectedComponentPreviewAtom } from "@/atoms/previewAtoms";
|
||||
import { SelectedComponentDisplay } from "./SelectedComponentDisplay";
|
||||
|
||||
const showTokenBarAtom = atom(false);
|
||||
|
||||
export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
@@ -78,6 +81,9 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
const [, setMessages] = useAtom<Message[]>(chatMessagesAtom);
|
||||
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
|
||||
const [showTokenBar, setShowTokenBar] = useAtom(showTokenBarAtom);
|
||||
const [selectedComponent, setSelectedComponent] = useAtom(
|
||||
selectedComponentPreviewAtom,
|
||||
);
|
||||
|
||||
// Use the attachments hook
|
||||
const {
|
||||
@@ -148,6 +154,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
|
||||
const currentInput = inputValue;
|
||||
setInputValue("");
|
||||
setSelectedComponent(null);
|
||||
|
||||
// Send message with attachments and clear them after sending
|
||||
await streamMessage({
|
||||
@@ -155,6 +162,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
chatId,
|
||||
attachments,
|
||||
redo: false,
|
||||
selectedComponent,
|
||||
});
|
||||
clearAttachments();
|
||||
posthog.capture("chat:submit");
|
||||
@@ -282,6 +290,8 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
<SelectedComponentDisplay />
|
||||
|
||||
{/* Use the AttachmentsList component */}
|
||||
<AttachmentsList
|
||||
attachments={attachments}
|
||||
|
||||
@@ -20,87 +20,87 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
|
||||
message.role === "assistant" ? "justify-start" : "justify-end"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`rounded-lg p-2 mt-2 ${
|
||||
message.role === "assistant"
|
||||
? "w-full max-w-3xl mx-auto"
|
||||
: "bg-(--sidebar-accent)"
|
||||
}`}
|
||||
>
|
||||
{message.role === "assistant" &&
|
||||
!message.content &&
|
||||
isStreaming &&
|
||||
isLastMessage ? (
|
||||
<div className="flex h-6 items-center space-x-2 p-2">
|
||||
<motion.div
|
||||
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration: 0.4,
|
||||
ease: "easeOut",
|
||||
repeatDelay: 1.2,
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration: 0.4,
|
||||
ease: "easeOut",
|
||||
delay: 0.4,
|
||||
repeatDelay: 1.2,
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration: 0.4,
|
||||
ease: "easeOut",
|
||||
delay: 0.8,
|
||||
repeatDelay: 1.2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="prose dark:prose-invert prose-headings:mb-2 prose-p:my-1 prose-pre:my-0 max-w-none"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{message.role === "assistant" ? (
|
||||
<>
|
||||
<DyadMarkdownParser content={message.content} />
|
||||
{isLastMessage && isStreaming && (
|
||||
<div className="mt-4 ml-4 relative w-5 h-5 animate-spin">
|
||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full"></div>
|
||||
<div className="absolute bottom-0 left-0 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full opacity-80"></div>
|
||||
<div className="absolute bottom-0 right-0 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full opacity-60"></div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<VanillaMarkdownParser content={message.content} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{message.approvalState && (
|
||||
<div className="mt-2 flex items-center justify-end space-x-1 text-xs">
|
||||
{message.approvalState === "approved" ? (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
<span>Approved</span>
|
||||
</>
|
||||
) : message.approvalState === "rejected" ? (
|
||||
<>
|
||||
<XCircle className="h-4 w-4 text-red-500" />
|
||||
<span>Rejected</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
<div className={`mt-2 w-full max-w-3xl mx-auto`}>
|
||||
<div
|
||||
className={`rounded-lg p-2 ${
|
||||
message.role === "assistant" ? "" : "ml-24 bg-(--sidebar-accent)"
|
||||
}`}
|
||||
>
|
||||
{message.role === "assistant" &&
|
||||
!message.content &&
|
||||
isStreaming &&
|
||||
isLastMessage ? (
|
||||
<div className="flex h-6 items-center space-x-2 p-2">
|
||||
<motion.div
|
||||
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration: 0.4,
|
||||
ease: "easeOut",
|
||||
repeatDelay: 1.2,
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration: 0.4,
|
||||
ease: "easeOut",
|
||||
delay: 0.4,
|
||||
repeatDelay: 1.2,
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration: 0.4,
|
||||
ease: "easeOut",
|
||||
delay: 0.8,
|
||||
repeatDelay: 1.2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="prose dark:prose-invert prose-headings:mb-2 prose-p:my-1 prose-pre:my-0 max-w-none"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{message.role === "assistant" ? (
|
||||
<>
|
||||
<DyadMarkdownParser content={message.content} />
|
||||
{isLastMessage && isStreaming && (
|
||||
<div className="mt-4 ml-4 relative w-5 h-5 animate-spin">
|
||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full"></div>
|
||||
<div className="absolute bottom-0 left-0 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full opacity-80"></div>
|
||||
<div className="absolute bottom-0 right-0 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full opacity-60"></div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<VanillaMarkdownParser content={message.content} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{message.approvalState && (
|
||||
<div className="mt-2 flex items-center justify-end space-x-1 text-xs">
|
||||
{message.approvalState === "approved" ? (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
<span>Approved</span>
|
||||
</>
|
||||
) : message.approvalState === "rejected" ? (
|
||||
<>
|
||||
<XCircle className="h-4 w-4 text-red-500" />
|
||||
<span>Rejected</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
47
src/components/chat/SelectedComponentDisplay.tsx
Normal file
47
src/components/chat/SelectedComponentDisplay.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { selectedComponentPreviewAtom } from "@/atoms/previewAtoms";
|
||||
import { useAtom } from "jotai";
|
||||
import { Code2, X } from "lucide-react";
|
||||
|
||||
export function SelectedComponentDisplay() {
|
||||
const [selectedComponent, setSelectedComponent] = useAtom(
|
||||
selectedComponentPreviewAtom,
|
||||
);
|
||||
|
||||
if (!selectedComponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-2 pb-1" data-testid="selected-component-display">
|
||||
<div className="flex items-center justify-between rounded-md bg-indigo-600/10 px-2 py-1 text-sm">
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<Code2
|
||||
size={16}
|
||||
className="flex-shrink-0 text-indigo-600 dark:text-indigo-400"
|
||||
/>
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
<span
|
||||
className="truncate font-medium text-indigo-800 dark:text-indigo-300"
|
||||
title={selectedComponent.name}
|
||||
>
|
||||
{selectedComponent.name}
|
||||
</span>
|
||||
<span
|
||||
className="truncate text-xs text-indigo-600/80 dark:text-indigo-400/80"
|
||||
title={`${selectedComponent.relativePath}:${selectedComponent.lineNumber}`}
|
||||
>
|
||||
{selectedComponent.relativePath}:{selectedComponent.lineNumber}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedComponent(null)}
|
||||
className="ml-2 flex-shrink-0 rounded-full p-0.5 hover:bg-indigo-600/20"
|
||||
title="Deselect component"
|
||||
>
|
||||
<X size={18} className="text-indigo-600 dark:text-indigo-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ChevronDown,
|
||||
Lightbulb,
|
||||
ChevronRight,
|
||||
MousePointerClick,
|
||||
} from "lucide-react";
|
||||
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
@@ -29,6 +30,14 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||
import { selectedComponentPreviewAtom } from "@/atoms/previewAtoms";
|
||||
import { ComponentSelection } from "@/ipc/ipc_types";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
interface ErrorBannerProps {
|
||||
error: string | undefined;
|
||||
@@ -165,11 +174,30 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
}, [routerContent]);
|
||||
|
||||
// Navigation state
|
||||
const [isComponentSelectorInitialized, setIsComponentSelectorInitialized] =
|
||||
useState(false);
|
||||
const [canGoBack, setCanGoBack] = useState(false);
|
||||
const [canGoForward, setCanGoForward] = useState(false);
|
||||
const [navigationHistory, setNavigationHistory] = useState<string[]>([]);
|
||||
const [currentHistoryPosition, setCurrentHistoryPosition] = useState(0);
|
||||
const [selectedComponentPreview, setSelectedComponentPreview] = useAtom(
|
||||
selectedComponentPreviewAtom,
|
||||
);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const [isPicking, setIsPicking] = useState(false);
|
||||
|
||||
// Deactivate component selector when selection is cleared
|
||||
useEffect(() => {
|
||||
if (!selectedComponentPreview) {
|
||||
if (iframeRef.current?.contentWindow) {
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{ type: "deactivate-dyad-component-selector" },
|
||||
"*",
|
||||
);
|
||||
}
|
||||
setIsPicking(false);
|
||||
}
|
||||
}, [selectedComponentPreview]);
|
||||
|
||||
// Add message listener for iframe errors and navigation events
|
||||
useEffect(() => {
|
||||
@@ -179,6 +207,18 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data?.type === "dyad-component-selector-initialized") {
|
||||
setIsComponentSelectorInitialized(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data?.type === "dyad-component-selected") {
|
||||
console.log("Component picked:", event.data);
|
||||
setSelectedComponentPreview(parseComponentSelection(event.data));
|
||||
setIsPicking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, payload } = event.data as {
|
||||
type:
|
||||
| "window-error"
|
||||
@@ -262,6 +302,8 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
selectedAppId,
|
||||
errorMessage,
|
||||
setErrorMessage,
|
||||
setIsComponentSelectorInitialized,
|
||||
setSelectedComponentPreview,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -280,6 +322,22 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
}
|
||||
}, [appUrl]);
|
||||
|
||||
// Function to activate component selector in the iframe
|
||||
const handleActivateComponentSelector = () => {
|
||||
if (iframeRef.current?.contentWindow) {
|
||||
const newIsPicking = !isPicking;
|
||||
setIsPicking(newIsPicking);
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{
|
||||
type: newIsPicking
|
||||
? "activate-dyad-component-selector"
|
||||
: "deactivate-dyad-component-selector",
|
||||
},
|
||||
"*",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to navigate back
|
||||
const handleNavigateBack = () => {
|
||||
if (canGoBack && iframeRef.current?.contentWindow) {
|
||||
@@ -371,6 +429,33 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
<div className="flex items-center p-2 border-b space-x-2 ">
|
||||
{/* Navigation Buttons */}
|
||||
<div className="flex space-x-1">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={handleActivateComponentSelector}
|
||||
className={`p-1 rounded transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||
isPicking
|
||||
? "bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700"
|
||||
: " text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900"
|
||||
}`}
|
||||
disabled={
|
||||
loading || !selectedAppId || !isComponentSelectorInitialized
|
||||
}
|
||||
data-testid="preview-pick-element-button"
|
||||
>
|
||||
<MousePointerClick size={16} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{isPicking
|
||||
? "Deactivate component selector"
|
||||
: "Select component"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<button
|
||||
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed dark:text-gray-300"
|
||||
disabled={!canGoBack || loading || !selectedAppId}
|
||||
@@ -486,3 +571,48 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function parseComponentSelection(data: any): ComponentSelection | null {
|
||||
if (
|
||||
!data ||
|
||||
data.type !== "dyad-component-selected" ||
|
||||
typeof data.id !== "string" ||
|
||||
typeof data.name !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id, name } = data;
|
||||
|
||||
// The id is expected to be in the format "filepath:line:column"
|
||||
const parts = id.split(":");
|
||||
if (parts.length < 3) {
|
||||
console.error(`Invalid component selection id format: "${id}"`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const columnStr = parts.pop();
|
||||
const lineStr = parts.pop();
|
||||
const relativePath = parts.join(":");
|
||||
|
||||
if (!columnStr || !lineStr || !relativePath) {
|
||||
console.error(`Could not parse component selection from id: "${id}"`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const lineNumber = parseInt(lineStr, 10);
|
||||
const columnNumber = parseInt(columnStr, 10);
|
||||
|
||||
if (isNaN(lineNumber) || isNaN(columnNumber)) {
|
||||
console.error(`Could not parse line/column from id: "${id}"`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
relativePath,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user