mirror of https://github.com/kortix-ai/suna.git
Merge branch 'main' of https://github.com/escapade-mckv/suna into feat/ui
This commit is contained in:
commit
b88433c328
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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 >
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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: '',
|
||||
|
|
Loading…
Reference in New Issue