Merge branch 'main' of https://github.com/escapade-mckv/suna into feat/ui

This commit is contained in:
Soumyadas15 2025-05-05 21:12:56 +05:30
commit b88433c328
6 changed files with 869 additions and 820 deletions

View File

@ -0,0 +1,16 @@
DROP POLICY IF EXISTS thread_select_policy ON threads;
CREATE POLICY thread_select_policy ON threads
FOR SELECT
USING (
is_public IS TRUE
OR basejump.has_role_on_account(account_id) = true
OR EXISTS (
SELECT 1 FROM projects
WHERE projects.project_id = threads.project_id
AND (
projects.is_public IS TRUE
OR basejump.has_role_on_account(projects.account_id) = true
)
)
);

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,10 @@ import {
Plus,
MessagesSquare,
Loader2,
} from 'lucide-react';
import { toast } from 'sonner';
import { usePathname, useRouter } from 'next/navigation';
Share2
} from "lucide-react"
import { toast } from "sonner"
import { usePathname, useRouter } from "next/navigation"
import {
DropdownMenu,
@ -32,12 +33,13 @@ import {
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { getProjects, getThreads, Project, deleteThread } from '@/lib/api';
import Link from 'next/link';
import { DeleteConfirmationDialog } from '@/components/thread/DeleteConfirmationDialog';
import { useDeleteOperation } from '@/contexts/DeleteOperationContext';
TooltipTrigger
} from "@/components/ui/tooltip"
import { getProjects, getThreads, Project, deleteThread } from "@/lib/api"
import Link from "next/link"
import { ShareModal } from "./share-modal"
import { DeleteConfirmationDialog } from "@/components/thread/DeleteConfirmationDialog"
import { useDeleteOperation } from '@/contexts/DeleteOperationContext'
// Thread with associated project info for display in sidebar
type ThreadWithProject = {
@ -49,19 +51,18 @@ type ThreadWithProject = {
};
export function NavAgents() {
const { isMobile, state } = useSidebar();
const [threads, setThreads] = useState<ThreadWithProject[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [loadingThreadId, setLoadingThreadId] = useState<string | null>(null);
const pathname = usePathname();
const router = useRouter();
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [threadToDelete, setThreadToDelete] = useState<{
id: string;
name: string;
} | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const isNavigatingRef = useRef(false);
const { isMobile, state } = useSidebar()
const [threads, setThreads] = useState<ThreadWithProject[]>([])
const [isLoading, setIsLoading] = useState(true)
const [loadingThreadId, setLoadingThreadId] = useState<string | null>(null)
const [showShareModal, setShowShareModal] = useState(false)
const [selectedItem, setSelectedItem] = useState<{ threadId: string, projectId: string } | null>(null)
const pathname = usePathname()
const router = useRouter()
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
const [threadToDelete, setThreadToDelete] = useState<{ id: string; name: string } | null>(null)
const [isDeleting, setIsDeleting] = useState(false)
const isNavigatingRef = useRef(false)
const { performDelete, isOperationInProgress } = useDeleteOperation();
const isPerformingActionRef = useRef(false);
@ -82,12 +83,8 @@ export function NavAgents() {
}
// Get all projects
const projects = (await getProjects()) as Project[];
console.log(
'Projects loaded:',
projects.length,
projects.map((p) => ({ id: p.id, name: p.name })),
);
const projects = await getProjects() as Project[]
console.log("Projects loaded:", projects.length, projects.map(p => ({ id: p.id, name: p.name })));
// If no projects are found, the user might not be logged in
if (projects.length === 0) {
@ -102,15 +99,8 @@ export function NavAgents() {
});
// Get all threads at once
const allThreads = await getThreads();
console.log(
'Threads loaded:',
allThreads.length,
allThreads.map((t) => ({
thread_id: t.thread_id,
project_id: t.project_id,
})),
);
const allThreads = await getThreads()
console.log("Threads loaded:", allThreads.length, allThreads.map(t => ({ thread_id: t.thread_id, project_id: t.project_id })));
// Create display objects for threads with their project info
const threadsWithProjects: ThreadWithProject[] = [];
@ -129,9 +119,7 @@ export function NavAgents() {
continue;
}
console.log(
`✅ Thread ${thread.thread_id} matched with project "${project.name}" (${projectId})`,
);
console.log(`✅ Thread ${thread.thread_id} matched with project "${project.name}" (${projectId})`);
// Add to our list
threadsWithProjects.push({
@ -170,14 +158,14 @@ export function NavAgents() {
const { projectId, updatedData } = customEvent.detail;
// Update just the name for the threads with the matching project ID
setThreads((prevThreads) => {
const updatedThreads = prevThreads.map((thread) =>
setThreads(prevThreads => {
const updatedThreads = prevThreads.map(thread =>
thread.projectId === projectId
? {
...thread,
projectName: updatedData.name,
}
: thread,
...thread,
projectName: updatedData.name,
}
: thread
);
// Return the threads without re-sorting immediately
@ -190,10 +178,7 @@ export function NavAgents() {
};
// Add event listener
window.addEventListener(
'project-updated',
handleProjectUpdate as EventListener,
);
window.addEventListener('project-updated', handleProjectUpdate as EventListener);
// Cleanup
return () => {
@ -217,12 +202,12 @@ export function NavAgents() {
isNavigatingRef.current = false;
};
window.addEventListener('popstate', handleNavigationComplete);
window.addEventListener("popstate", handleNavigationComplete);
return () => {
window.removeEventListener('popstate', handleNavigationComplete);
// Ensure we clean up any leftover styles
document.body.style.pointerEvents = 'auto';
document.body.style.pointerEvents = "auto";
};
}, []);
@ -233,15 +218,11 @@ export function NavAgents() {
}, [pathname]);
// Function to handle thread click with loading state
const handleThreadClick = (
e: React.MouseEvent<HTMLAnchorElement>,
threadId: string,
url: string,
) => {
e.preventDefault();
setLoadingThreadId(threadId);
router.push(url);
};
const handleThreadClick = (e: React.MouseEvent<HTMLAnchorElement>, threadId: string, url: string) => {
e.preventDefault()
setLoadingThreadId(threadId)
router.push(url)
}
// Function to handle thread deletion
const handleDeleteThread = async (threadId: string, threadName: string) => {
@ -279,7 +260,7 @@ export function NavAgents() {
await deleteThread(threadId);
// Update the thread list
setThreads((prev) => prev.filter((t) => t.threadId !== threadId));
setThreads(prev => prev.filter(t => t.threadId !== threadId));
// Show success message
toast.success('Conversation deleted successfully');
@ -413,16 +394,12 @@ export function NavAgents() {
side={isMobile ? 'bottom' : 'right'}
align={isMobile ? 'end' : 'start'}
>
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(
window.location.origin + thread.url,
);
toast.success('Link copied to clipboard');
}}
>
<LinkIcon className="text-muted-foreground" />
<span>Copy Link</span>
<DropdownMenuItem onClick={() => {
setSelectedItem({ threadId: thread?.threadId, projectId: thread?.projectId })
setShowShareModal(true)
}}>
<Share2 className="text-muted-foreground" />
<span>Share Chat</span>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<a
@ -463,6 +440,12 @@ export function NavAgents() {
</SidebarMenuItem>
)}
</SidebarMenu>
<ShareModal
isOpen={showShareModal}
onClose={() => setShowShareModal(false)}
threadId={selectedItem?.threadId}
projectId={selectedItem?.projectId}
/>
{threadToDelete && (
<DeleteConfirmationDialog

View File

@ -0,0 +1,236 @@
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogTitle,
} from "@/components/ui/dialog";
import { X, Copy, Share2, Link, Link2Off, Check } from "lucide-react";
import { toast } from "sonner";
import { getThread, updateProject, updateThread } from "@/lib/api";
interface SocialShareOption {
name: string;
icon: JSX.Element;
onClick: () => void;
}
interface ShareModalProps {
isOpen: boolean;
onClose: () => void;
threadId?: string;
projectId?: string;
}
export function ShareModal({ isOpen, onClose, threadId, projectId }: ShareModalProps) {
const [shareLink, setShareLink] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isChecking, setIsChecking] = useState(false);
const [isCopying, setIsCopying] = useState(false);
useEffect(() => {
if (isOpen && threadId) {
checkShareStatus();
}
}, [isOpen, threadId]);
const checkShareStatus = async () => {
if (!threadId) return;
setIsChecking(true);
try {
const threadData = await getThread(threadId);
if (threadData?.is_public) {
const publicUrl = generateShareLink();
setShareLink(publicUrl);
} else {
setShareLink(null);
}
} catch (error) {
console.error("Error checking share status:", error);
toast.error("Failed to check sharing status");
setShareLink(null);
} finally {
setIsChecking(false);
}
};
const generateShareLink = () => {
if (!threadId) return "";
return `${process.env.NEXT_PUBLIC_URL || window.location.origin}/share/${threadId}`;
};
const createShareLink = async () => {
if (!threadId) return;
setIsLoading(true);
try {
// Use the API to mark the thread as public
await updatePublicStatus(true);
const generatedLink = generateShareLink();
setShareLink(generatedLink);
toast.success("Shareable link created successfully");
} catch (error) {
console.error("Error creating share link:", error);
toast.error("Failed to create shareable link");
} finally {
setIsLoading(false);
}
};
const removeShareLink = async () => {
if (!threadId) return;
setIsLoading(true);
try {
// Use the API to mark the thread as private
await updatePublicStatus(false);
setShareLink(null);
toast.success("Shareable link removed");
} catch (error) {
console.error("Error removing share link:", error);
toast.error("Failed to remove shareable link");
} finally {
setIsLoading(false);
}
};
const updatePublicStatus = async (isPublic: boolean) => {
console.log("Updating public status for thread:", threadId, "and project:", projectId, "to", isPublic);
if (!threadId) return;
await updateProject(projectId, { is_public: isPublic });
await updateThread(threadId, { is_public: isPublic });
};
const copyToClipboard = () => {
if (shareLink) {
setIsCopying(true);
navigator.clipboard.writeText(shareLink);
toast.success("Link copied to clipboard");
setTimeout(() => {
setIsCopying(false);
}, 500);
}
};
const socialOptions: SocialShareOption[] = [
{
name: "LinkedIn",
icon: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" className="size-5 text-current">
<path fill="currentColor" d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>,
onClick: () => {
if (shareLink) {
window.open(`https://www.linkedin.com/shareArticle?url=${encodeURIComponent(shareLink)}&text=Shared conversation`, '_blank');
}
}
},
{
name: "X",
icon: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" className="size-5 text-current">
<path fill="currentColor" d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" />
</svg>,
onClick: () => {
if (shareLink) {
window.open(`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareLink)}&text=Shared conversation`, '_blank');
}
}
}
];
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md p-0 gap-0 overflow-hidden">
<DialogTitle className="p-4 font-medium">
Share Chat
<button
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onClick={onClose}>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
</DialogTitle>
<div className="p-4 space-y-6">
{isChecking ? (
<div className="flex flex-col items-center justify-center py-10">
<div className="h-6 w-6 border-2 border-t-primary border-primary/30 rounded-full animate-spin"></div>
<p className="text-sm text-muted-foreground mt-4">Checking share status...</p>
</div>
) : shareLink ? (
<>
<p className="text-sm text-muted-foreground">
Your chat is now publicly accessible with the link below. Anyone with this link can view this conversation.
</p>
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0 sm:space-x-2">
<div className="relative flex flex-1 items-center">
<input
type="text"
readOnly
value={shareLink}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
/>
</div>
<Button variant="outline" className="h-10 px-4 py-2" onClick={copyToClipboard}>
{isCopying ? <Check className=" h-4 w-4" /> : <Copy className=" h-4 w-4" />}
</Button>
</div>
<div className="flex justify-center gap-6 pt-2">
{socialOptions.map((option, index) => (
<button
key={index}
className="p-2 rounded-full flex flex-col items-center group"
onClick={option.onClick}
>
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center text-muted-foreground group-hover:bg-primary group-hover:text-primary-foreground transition-all duration-200">
{option.icon}
</div>
<span className="text-xs mt-1 group-hover:text-primary transition-colors duration-200">{option.name}</span>
</button>
))}
</div>
<div className="pt-1">
<Button
variant="default"
className="w-full"
onClick={removeShareLink}
disabled={isLoading}
>
<Link2Off className="mr-2 h-4 w-4" />
{isLoading ? "Processing..." : "Remove link"}
</Button>
</div>
</>
) : (
<>
<div className="flex flex-col items-center justify-center py-6 text-center">
<div className="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center mb-4">
<Share2 className="h-6 w-6 text-primary" />
</div>
<h3 className="text-lg font-medium mb-2">Share this chat</h3>
<p className="text-sm text-muted-foreground max-w-sm mb-6">
Create a shareable link that allows others to view this conversation.
</p>
<Button
onClick={createShareLink}
className="w-full max-w-xs"
disabled={isLoading}
>
<Link className="mr-2 h-4 w-4" />
{isLoading ? "Creating link..." : "Create shareable link"}
</Button>
</div>
</>
)}
</div>
</DialogContent >
</Dialog >
);
}

View File

@ -1,22 +1,23 @@
'use client';
import { Button } from '@/components/ui/button';
import { FolderOpen, Link, PanelRightOpen, Check, X, Menu } from 'lucide-react';
import { usePathname } from 'next/navigation';
import { toast } from 'sonner';
import { Button } from "@/components/ui/button"
import { FolderOpen, Link, PanelRightOpen, Check, X, Menu, Share2 } from "lucide-react"
import { usePathname } from "next/navigation"
import { toast } from "sonner"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useState, useRef, KeyboardEvent } from 'react';
import { Input } from '@/components/ui/input';
import { updateProject } from '@/lib/api';
import { Skeleton } from '@/components/ui/skeleton';
import { useIsMobile } from '@/hooks/use-mobile';
import { cn } from '@/lib/utils';
import { useSidebar } from '@/components/ui/sidebar';
} from "@/components/ui/tooltip"
import { useState, useRef, KeyboardEvent } from "react"
import { Input } from "@/components/ui/input"
import { updateProject } from "@/lib/api"
import { Skeleton } from "@/components/ui/skeleton"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { useSidebar } from "@/components/ui/sidebar"
import { ShareModal } from "@/components/sidebar/share-modal"
interface ThreadSiteHeaderProps {
threadId: string;
@ -37,18 +38,18 @@ export function SiteHeader({
onProjectRenamed,
isMobileView,
}: ThreadSiteHeaderProps) {
const pathname = usePathname();
const [isEditing, setIsEditing] = useState(false);
const [editName, setEditName] = useState(projectName);
const inputRef = useRef<HTMLInputElement>(null);
const isMobile = useIsMobile() || isMobileView;
const { setOpenMobile } = useSidebar();
const pathname = usePathname()
const [isEditing, setIsEditing] = useState(false)
const [editName, setEditName] = useState(projectName)
const inputRef = useRef<HTMLInputElement>(null)
const [showShareModal, setShowShareModal] = useState(false)
const copyCurrentUrl = () => {
const url = window.location.origin + pathname;
navigator.clipboard.writeText(url);
toast.success('URL copied to clipboard');
};
const isMobile = useIsMobile() || isMobileView
const { setOpenMobile } = useSidebar()
const openShareModal = () => {
setShowShareModal(true)
}
const startEditing = () => {
setEditName(projectName);
@ -80,9 +81,7 @@ export function SiteHeader({
return;
}
const updatedProject = await updateProject(projectId, {
name: editName,
});
const updatedProject = await updateProject(projectId, { name: editName })
if (updatedProject) {
onProjectRenamed?.(editName);
toast.success('Project renamed successfully');
@ -98,8 +97,8 @@ export function SiteHeader({
}
}
setIsEditing(false);
};
setIsEditing(false)
}
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
@ -110,131 +109,137 @@ export function SiteHeader({
};
return (
<header
className={cn(
'bg-background sticky top-0 flex h-14 shrink-0 items-center gap-2 z-20 w-full',
isMobile && 'px-2',
)}
>
{isMobile && (
<Button
variant="ghost"
size="icon"
onClick={() => setOpenMobile(true)}
className="h-9 w-9 mr-1"
aria-label="Open sidebar"
>
<Menu className="h-4 w-4" />
</Button>
)}
<div className="flex flex-1 items-center gap-2 px-3">
{isEditing ? (
<div className="flex items-center gap-1">
<Input
ref={inputRef}
value={editName}
onChange={(e) => setEditName(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={saveNewName}
className="h-8 w-auto min-w-[180px] text-base font-medium"
maxLength={50}
/>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={saveNewName}
>
<Check className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={cancelEditing}
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
) : !projectName || projectName === 'Project' ? (
<Skeleton className="h-5 w-32" />
) : (
<div
className="text-base font-medium text-muted-foreground hover:text-foreground cursor-pointer flex items-center"
onClick={startEditing}
title="Click to rename project"
>
{projectName}
</div>
)}
</div>
<div className="flex items-center gap-1 pr-4">
{isMobile ? (
// Mobile view - only show the side panel toggle
<>
<header className={cn(
"bg-background sticky top-0 flex h-14 shrink-0 items-center gap-2 z-20 w-full",
isMobile && "px-2"
)}>
{isMobile && (
<Button
variant="ghost"
size="icon"
onClick={onToggleSidePanel}
className="h-9 w-9 cursor-pointer"
aria-label="Toggle computer panel"
onClick={() => setOpenMobile(true)}
className="h-9 w-9 mr-1"
aria-label="Open sidebar"
>
<PanelRightOpen className="h-4 w-4" />
<Menu className="h-4 w-4" />
</Button>
) : (
// Desktop view - show all buttons with tooltips
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={onViewFiles}
className="h-9 w-9 cursor-pointer"
>
<FolderOpen className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>View Files in Task</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={copyCurrentUrl}
className="h-9 w-9 cursor-pointer"
>
<Link className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copy Link</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={onToggleSidePanel}
className="h-9 w-9 cursor-pointer"
>
<PanelRightOpen className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Toggle Computer Preview (CMD+I)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</header>
);
}
<div className="flex flex-1 items-center gap-2 px-3">
{isEditing ? (
<div className="flex items-center gap-1">
<Input
ref={inputRef}
value={editName}
onChange={(e) => setEditName(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={saveNewName}
className="h-8 w-auto min-w-[180px] text-base font-medium"
maxLength={50}
/>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={saveNewName}
>
<Check className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={cancelEditing}
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
) : !projectName || projectName === 'Project' ? (
<Skeleton className="h-5 w-32" />
) : (
<div
className="text-base font-medium text-muted-foreground hover:text-foreground cursor-pointer flex items-center"
onClick={startEditing}
title="Click to rename project"
>
{projectName}
</div>
)}
</div>
<div className="flex items-center gap-1 pr-4">
{isMobile ? (
// Mobile view - only show the side panel toggle
<Button
variant="ghost"
size="icon"
onClick={onToggleSidePanel}
className="h-9 w-9 cursor-pointer"
aria-label="Toggle computer panel"
>
<PanelRightOpen className="h-4 w-4" />
</Button>
) : (
// Desktop view - show all buttons with tooltips
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={onViewFiles}
className="h-9 w-9 cursor-pointer"
>
<FolderOpen className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>View Files in Task</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={openShareModal}
className="h-9 w-9 cursor-pointer"
>
<Share2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Share Chat</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={onToggleSidePanel}
className="h-9 w-9 cursor-pointer"
>
<PanelRightOpen className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Toggle Computer Preview (CMD+I)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</header>
<ShareModal
isOpen={showShareModal}
onClose={() => setShowShareModal(false)}
threadId={threadId}
projectId={projectId}
/>
</>
)
}

View File

@ -236,6 +236,7 @@ export const getProject = async (projectId: string): Promise<Project> => {
name: data.name || '',
description: data.description || '',
account_id: data.account_id,
is_public: data.is_public || false,
created_at: data.created_at,
sandbox: data.sandbox || {
id: '',