mirror of https://github.com/kortix-ai/suna.git
frontend
This commit is contained in:
parent
a4f01b6092
commit
797b496863
|
@ -15,6 +15,7 @@ CREATE TABLE threads (
|
|||
thread_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id UUID REFERENCES basejump.accounts(id) ON DELETE CASCADE,
|
||||
project_id UUID REFERENCES projects(project_id) ON DELETE CASCADE,
|
||||
is_public BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
|
||||
);
|
||||
|
@ -113,6 +114,7 @@ CREATE POLICY project_delete_policy ON projects
|
|||
CREATE POLICY thread_select_policy ON threads
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_public = TRUE OR
|
||||
basejump.has_role_on_account(account_id) = true OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM projects
|
||||
|
@ -163,6 +165,7 @@ CREATE POLICY agent_run_select_policy ON agent_runs
|
|||
LEFT JOIN projects ON threads.project_id = projects.project_id
|
||||
WHERE threads.thread_id = agent_runs.thread_id
|
||||
AND (
|
||||
threads.is_public = TRUE OR
|
||||
basejump.has_role_on_account(threads.account_id) = true OR
|
||||
basejump.has_role_on_account(projects.account_id) = true
|
||||
)
|
||||
|
@ -220,6 +223,7 @@ CREATE POLICY message_select_policy ON messages
|
|||
LEFT JOIN projects ON threads.project_id = projects.project_id
|
||||
WHERE threads.thread_id = messages.thread_id
|
||||
AND (
|
||||
threads.is_public = TRUE OR
|
||||
basejump.has_role_on_account(threads.account_id) = true OR
|
||||
basejump.has_role_on_account(projects.account_id) = true
|
||||
)
|
||||
|
@ -286,12 +290,18 @@ DECLARE
|
|||
current_role TEXT;
|
||||
latest_summary_id UUID;
|
||||
latest_summary_time TIMESTAMP WITH TIME ZONE;
|
||||
is_thread_public BOOLEAN;
|
||||
BEGIN
|
||||
-- Get current role
|
||||
SELECT current_user INTO current_role;
|
||||
|
||||
-- Skip access check for service_role
|
||||
IF current_role = 'authenticated' THEN
|
||||
-- Check if thread is public
|
||||
SELECT is_public INTO is_thread_public
|
||||
FROM threads
|
||||
WHERE thread_id = p_thread_id;
|
||||
|
||||
-- Skip access check for service_role or public threads
|
||||
IF current_role = 'authenticated' AND NOT is_thread_public THEN
|
||||
-- Check if thread exists and user has access
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM threads t
|
||||
|
|
|
@ -417,10 +417,14 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
|||
|
||||
if (!isMounted) return;
|
||||
|
||||
console.log('[PAGE] Thread data loaded:', threadData);
|
||||
|
||||
if (threadData?.project_id) {
|
||||
console.log('[PAGE] Getting project data for project_id:', threadData.project_id);
|
||||
const projectData = await getProject(threadData.project_id);
|
||||
if (isMounted && projectData) {
|
||||
console.log('[PAGE] Project data loaded:', projectData);
|
||||
console.log('[PAGE] Project ID:', projectData.id);
|
||||
console.log('[PAGE] Project sandbox data:', projectData.sandbox);
|
||||
|
||||
// Set project data
|
||||
|
@ -1089,7 +1093,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
|||
<SiteHeader
|
||||
threadId={threadId}
|
||||
projectName={projectName}
|
||||
projectId={project?.id ?? null}
|
||||
projectId={project?.id || ""}
|
||||
onViewFiles={handleOpenFileViewer}
|
||||
onToggleSidePanel={toggleSidePanel}
|
||||
/>
|
||||
|
@ -1109,7 +1113,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
|||
toolCalls={[]}
|
||||
currentIndex={0}
|
||||
onNavigate={handleSidePanelNavigate}
|
||||
project={project}
|
||||
project={project || undefined}
|
||||
agentStatus="error"
|
||||
/>
|
||||
</div>
|
||||
|
@ -1122,7 +1126,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
|||
<SiteHeader
|
||||
threadId={threadId}
|
||||
projectName={projectName}
|
||||
projectId={project?.id ?? null}
|
||||
projectId={project?.id || ""}
|
||||
onViewFiles={handleOpenFileViewer}
|
||||
onToggleSidePanel={toggleSidePanel}
|
||||
onProjectRenamed={handleProjectRenamed}
|
||||
|
@ -1385,7 +1389,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
|||
agentStatus={agentStatus}
|
||||
currentIndex={currentToolIndex}
|
||||
onNavigate={handleSidePanelNavigate}
|
||||
project={project}
|
||||
project={project || undefined}
|
||||
renderAssistantMessage={toolViewAssistant}
|
||||
renderToolResult={toolViewResult}
|
||||
/>
|
||||
|
@ -1396,7 +1400,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
|||
onOpenChange={setFileViewerOpen}
|
||||
sandboxId={sandboxId}
|
||||
initialFilePath={fileToView}
|
||||
project={project}
|
||||
project={project || undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { SidebarLeft } from "@/components/dashboard/sidebar/sidebar-left"
|
||||
import { SidebarLeft } from "@/components/sidebar/sidebar-left"
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Shared Conversation',
|
||||
description: 'View a shared AI conversation',
|
||||
openGraph: {
|
||||
title: 'Shared AI Conversation',
|
||||
description: 'View a shared AI conversation from Kortix Manus',
|
||||
images: ['/kortix-logo.png'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function ThreadLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <>{children}</>;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,71 +0,0 @@
|
|||
import * as React from "react"
|
||||
import { Check, ChevronRight } from "lucide-react"
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarSeparator,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
export function Calendars({
|
||||
calendars,
|
||||
}: {
|
||||
calendars: {
|
||||
name: string
|
||||
items: string[]
|
||||
}[]
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{calendars.map((calendar, index) => (
|
||||
<React.Fragment key={calendar.name}>
|
||||
<SidebarGroup key={calendar.name} className="py-0">
|
||||
<Collapsible
|
||||
defaultOpen={index === 0}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarGroupLabel
|
||||
asChild
|
||||
className="group/label text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground w-full text-sm"
|
||||
>
|
||||
<CollapsibleTrigger>
|
||||
{calendar.name}{" "}
|
||||
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
||||
</CollapsibleTrigger>
|
||||
</SidebarGroupLabel>
|
||||
<CollapsibleContent>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{calendar.items.map((item, index) => (
|
||||
<SidebarMenuItem key={item}>
|
||||
<SidebarMenuButton>
|
||||
<div
|
||||
data-active={index < 2}
|
||||
className="group/calendar-item border-sidebar-border text-sidebar-primary-foreground data-[active=true]:border-sidebar-primary data-[active=true]:bg-sidebar-primary flex aspect-square size-4 shrink-0 items-center justify-center rounded-xs border"
|
||||
>
|
||||
<Check className="hidden size-3 group-data-[active=true]/calendar-item:block" />
|
||||
</div>
|
||||
{item}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</SidebarGroup>
|
||||
<SidebarSeparator className="mx-0" />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { Button } from "@/components/ui/button"
|
||||
import Link from "next/link"
|
||||
import { Briefcase, ExternalLink } from "lucide-react"
|
||||
import { KortixProcessModal } from "@/components/dashboard/sidebar/kortix-enterprise-modal"
|
||||
import { KortixProcessModal } from "@/components/sidebar/kortix-enterprise-modal"
|
||||
|
||||
export function CTACard() {
|
||||
return (
|
|
@ -39,15 +39,15 @@ export function KortixProcessModal() {
|
|||
<DialogTitle className="sr-only">Custom AI Employees for your Business.</DialogTitle>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 h-[800px] overflow-hidden">
|
||||
{/* Info Panel */}
|
||||
<div className="p-8 md:p-12 flex flex-col bg-white dark:bg-black relative min-h-0">
|
||||
<div className="p-8 flex flex-col bg-white dark:bg-black relative min-h-0">
|
||||
<div className="relative z-10 h-full flex flex-col">
|
||||
<div className="mb-10">
|
||||
<div className="mb-8 mt-0">
|
||||
<Image
|
||||
src={isDarkMode ? "/kortix-logo-white.svg" : "/kortix-logo.svg"}
|
||||
alt="Kortix Logo"
|
||||
width={80}
|
||||
height={28}
|
||||
className="h-8 w-auto"
|
||||
width={60}
|
||||
height={21}
|
||||
className="h-6 w-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -117,6 +117,7 @@ export function KortixProcessModal() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-[#171717]">
|
||||
<Cal
|
||||
namespace="enterprise-demo"
|
||||
calLink="team/kortix/enterprise-demo"
|
||||
|
@ -126,6 +127,7 @@ export function KortixProcessModal() {
|
|||
hideEventTypeDetails: "false",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</DialogContent>
|
|
@ -34,87 +34,85 @@ import {
|
|||
TooltipContent,
|
||||
TooltipTrigger
|
||||
} from "@/components/ui/tooltip"
|
||||
import { getProjects, getThreads } from "@/lib/api"
|
||||
import { getProjects, getThreads, clearApiCache, Project, Thread } from "@/lib/api"
|
||||
import Link from "next/link"
|
||||
|
||||
// Define a type to handle potential database schema/API response differences
|
||||
type ProjectResponse = {
|
||||
id: string;
|
||||
project_id?: string;
|
||||
name: string;
|
||||
updated_at?: string;
|
||||
[key: string]: any; // Allow other properties
|
||||
}
|
||||
|
||||
// Agent type with project ID for easier updating
|
||||
type Agent = {
|
||||
projectId: string;
|
||||
// Thread with associated project info for display in sidebar
|
||||
type ThreadWithProject = {
|
||||
threadId: string;
|
||||
name: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
url: string;
|
||||
updatedAt: string; // Store updated_at for consistent sorting
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export function NavAgents() {
|
||||
const { isMobile, state } = useSidebar()
|
||||
const [agents, setAgents] = useState<Agent[]>([])
|
||||
const [threads, setThreads] = useState<ThreadWithProject[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [loadingAgentId, setLoadingAgentId] = useState<string | null>(null)
|
||||
const [loadingThreadId, setLoadingThreadId] = useState<string | null>(null)
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
|
||||
// Helper to sort agents by updated_at (most recent first)
|
||||
const sortAgents = (agentsList: Agent[]): Agent[] => {
|
||||
return [...agentsList].sort((a, b) => {
|
||||
// Helper to sort threads by updated_at (most recent first)
|
||||
const sortThreads = (threadsList: ThreadWithProject[]): ThreadWithProject[] => {
|
||||
return [...threadsList].sort((a, b) => {
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||
});
|
||||
};
|
||||
|
||||
// Function to load agents data
|
||||
const loadAgents = async (showLoading = true) => {
|
||||
// Function to load threads data with associated projects
|
||||
const loadThreadsWithProjects = async (showLoading = true) => {
|
||||
try {
|
||||
if (showLoading) {
|
||||
setIsLoading(true)
|
||||
}
|
||||
|
||||
// Get all projects
|
||||
const projectsData = await getProjects() as ProjectResponse[]
|
||||
const projects = await getProjects() as Project[]
|
||||
console.log("Projects loaded:", projects.length, projects.map(p => ({ id: p.id, name: p.name })));
|
||||
|
||||
// Create a map of projects by ID for faster lookups
|
||||
const projectsById = new Map<string, Project>();
|
||||
projects.forEach(project => {
|
||||
projectsById.set(project.id, project);
|
||||
});
|
||||
|
||||
// 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 })));
|
||||
|
||||
// For each project, find its matching threads
|
||||
const agentsList: Agent[] = []
|
||||
for (const project of projectsData) {
|
||||
// Get the project ID (handle potential different field names)
|
||||
const projectId = project.id || project.project_id || ''
|
||||
// Create display objects for threads with their project info
|
||||
const threadsWithProjects: ThreadWithProject[] = [];
|
||||
|
||||
// Get the updated_at timestamp (default to current time if not available)
|
||||
const updatedAt = project.updated_at || new Date().toISOString()
|
||||
for (const thread of allThreads) {
|
||||
const projectId = thread.project_id;
|
||||
// Skip threads without a project ID
|
||||
if (!projectId) continue;
|
||||
|
||||
// Match threads that belong to this project
|
||||
const projectThreads = allThreads.filter(thread =>
|
||||
thread.project_id === projectId
|
||||
)
|
||||
// Get the associated project
|
||||
const project = projectsById.get(projectId);
|
||||
if (!project) {
|
||||
console.log(`❌ Thread ${thread.thread_id} has project_id=${projectId} but no matching project found`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (projectThreads.length > 0) {
|
||||
// For each thread in this project, create an agent entry
|
||||
for (const thread of projectThreads) {
|
||||
agentsList.push({
|
||||
projectId,
|
||||
console.log(`✅ Thread ${thread.thread_id} matched with project "${project.name}" (${projectId})`);
|
||||
|
||||
// Add to our list
|
||||
threadsWithProjects.push({
|
||||
threadId: thread.thread_id,
|
||||
name: project.name || 'Unnamed Project',
|
||||
projectId: projectId,
|
||||
projectName: project.name || 'Unnamed Project',
|
||||
url: `/dashboard/agents/${thread.thread_id}`,
|
||||
updatedAt: thread.updated_at || updatedAt // Use thread update time if available
|
||||
})
|
||||
}
|
||||
}
|
||||
updatedAt: thread.updated_at || project.updated_at || new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Set agents, ensuring consistent sort order
|
||||
setAgents(sortAgents(agentsList))
|
||||
// Set threads, ensuring consistent sort order
|
||||
setThreads(sortThreads(threadsWithProjects))
|
||||
} catch (err) {
|
||||
console.error("Error loading agents for sidebar:", err)
|
||||
console.error("Error loading threads with projects:", err)
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
setIsLoading(false)
|
||||
|
@ -122,10 +120,10 @@ export function NavAgents() {
|
|||
}
|
||||
}
|
||||
|
||||
// Load agents dynamically from the API on initial load
|
||||
// Load threads dynamically from the API on initial load
|
||||
useEffect(() => {
|
||||
loadAgents(true)
|
||||
}, [])
|
||||
loadThreadsWithProjects(true);
|
||||
}, []);
|
||||
|
||||
// Listen for project-updated events to update the sidebar without full reload
|
||||
useEffect(() => {
|
||||
|
@ -134,25 +132,23 @@ export function NavAgents() {
|
|||
if (customEvent.detail) {
|
||||
const { projectId, updatedData } = customEvent.detail;
|
||||
|
||||
// Update just the name for the agents with the matching project ID
|
||||
// Don't update the timestamp here to prevent immediate re-sorting
|
||||
setAgents(prevAgents => {
|
||||
const updatedAgents = prevAgents.map(agent =>
|
||||
agent.projectId === projectId
|
||||
// Update just the name for the threads with the matching project ID
|
||||
setThreads(prevThreads => {
|
||||
const updatedThreads = prevThreads.map(thread =>
|
||||
thread.projectId === projectId
|
||||
? {
|
||||
...agent,
|
||||
name: updatedData.name,
|
||||
// Keep the original updatedAt timestamp locally
|
||||
...thread,
|
||||
projectName: updatedData.name,
|
||||
}
|
||||
: agent
|
||||
: thread
|
||||
);
|
||||
|
||||
// Return the agents without re-sorting immediately
|
||||
return updatedAgents;
|
||||
// Return the threads without re-sorting immediately
|
||||
return updatedThreads;
|
||||
});
|
||||
|
||||
// Silently refresh in background to fetch updated timestamp and re-sort
|
||||
setTimeout(() => loadAgents(false), 1000);
|
||||
setTimeout(() => loadThreadsWithProjects(false), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,13 +163,13 @@ export function NavAgents() {
|
|||
|
||||
// Reset loading state when navigation completes (pathname changes)
|
||||
useEffect(() => {
|
||||
setLoadingAgentId(null)
|
||||
setLoadingThreadId(null)
|
||||
}, [pathname])
|
||||
|
||||
// Function to handle agent click with loading state
|
||||
const handleAgentClick = (e: React.MouseEvent<HTMLAnchorElement>, threadId: string, url: string) => {
|
||||
// Function to handle thread click with loading state
|
||||
const handleThreadClick = (e: React.MouseEvent<HTMLAnchorElement>, threadId: string, url: string) => {
|
||||
e.preventDefault()
|
||||
setLoadingAgentId(threadId)
|
||||
setLoadingThreadId(threadId)
|
||||
router.push(url)
|
||||
}
|
||||
|
||||
|
@ -224,41 +220,41 @@ export function NavAgents() {
|
|||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))
|
||||
) : agents.length > 0 ? (
|
||||
// Show all agents
|
||||
) : threads.length > 0 ? (
|
||||
// Show all threads with project info
|
||||
<>
|
||||
{agents.map((agent, index) => {
|
||||
// Check if this agent is currently active
|
||||
const isActive = pathname.includes(agent.threadId);
|
||||
const isAgentLoading = loadingAgentId === agent.threadId;
|
||||
{threads.map((thread) => {
|
||||
// Check if this thread is currently active
|
||||
const isActive = pathname?.includes(thread.threadId) || false;
|
||||
const isThreadLoading = loadingThreadId === thread.threadId;
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={`agent-${agent.threadId}`}>
|
||||
<SidebarMenuItem key={`thread-${thread.threadId}`}>
|
||||
{state === "collapsed" ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SidebarMenuButton asChild className={isActive ? "bg-accent text-accent-foreground" : ""}>
|
||||
<Link href={agent.url} onClick={(e) => handleAgentClick(e, agent.threadId, agent.url)}>
|
||||
{isAgentLoading ? (
|
||||
<Link href={thread.url} onClick={(e) => handleThreadClick(e, thread.threadId, thread.url)}>
|
||||
{isThreadLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<MessagesSquare className="h-4 w-4" />
|
||||
)}
|
||||
<span>{agent.name}</span>
|
||||
<span>{thread.projectName}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{agent.name}</TooltipContent>
|
||||
<TooltipContent>{thread.projectName}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<SidebarMenuButton asChild className={isActive ? "bg-accent text-accent-foreground font-medium" : ""}>
|
||||
<Link href={agent.url} onClick={(e) => handleAgentClick(e, agent.threadId, agent.url)}>
|
||||
{isAgentLoading ? (
|
||||
<Link href={thread.url} onClick={(e) => handleThreadClick(e, thread.threadId, thread.url)}>
|
||||
{isThreadLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<MessagesSquare className="h-4 w-4" />
|
||||
)}
|
||||
<span>{agent.name}</span>
|
||||
<span>{thread.projectName}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
|
@ -276,14 +272,14 @@ export function NavAgents() {
|
|||
align={isMobile ? "end" : "start"}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => {
|
||||
navigator.clipboard.writeText(window.location.origin + agent.url)
|
||||
navigator.clipboard.writeText(window.location.origin + thread.url)
|
||||
toast.success("Link copied to clipboard")
|
||||
}}>
|
||||
<LinkIcon className="text-muted-foreground" />
|
||||
<span>Copy Link</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href={agent.url} target="_blank" rel="noopener noreferrer">
|
||||
<a href={thread.url} target="_blank" rel="noopener noreferrer">
|
||||
<ArrowUpRight className="text-muted-foreground" />
|
||||
<span>Open in New Tab</span>
|
||||
</a>
|
|
@ -3,10 +3,10 @@
|
|||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { NavAgents } from "@/components/dashboard/sidebar/nav-agents"
|
||||
import { NavUserWithTeams } from "@/components/dashboard/sidebar/nav-user-with-teams"
|
||||
import { KortixLogo } from "@/components/dashboard/sidebar/kortix-logo"
|
||||
import { CTACard } from "@/components/dashboard/sidebar/cta"
|
||||
import { NavAgents } from "@/components/sidebar/nav-agents"
|
||||
import { NavUserWithTeams } from "@/components/sidebar/nav-user-with-teams"
|
||||
import { KortixLogo } from "@/components/sidebar/kortix-logo"
|
||||
import { CTACard } from "@/components/sidebar/cta"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
|
@ -66,6 +66,13 @@ export function SiteHeader({
|
|||
|
||||
if (editName !== projectName) {
|
||||
try {
|
||||
if (!projectId) {
|
||||
toast.error("Cannot rename: Project ID is missing")
|
||||
setEditName(projectName)
|
||||
setIsEditing(false)
|
||||
return
|
||||
}
|
||||
|
||||
const updatedProject = await updateProject(projectId, { name: editName })
|
||||
if (updatedProject) {
|
||||
onProjectRenamed?.(editName)
|
||||
|
|
|
@ -16,12 +16,29 @@ const apiCache = {
|
|||
|
||||
getThreads: (projectId: string) => apiCache.threads.get(projectId || 'all'),
|
||||
setThreads: (projectId: string, data: any) => apiCache.threads.set(projectId || 'all', data),
|
||||
invalidateThreads: (projectId: string) => apiCache.threads.delete(projectId || 'all'),
|
||||
|
||||
getThreadMessages: (threadId: string) => apiCache.threadMessages.get(threadId),
|
||||
setThreadMessages: (threadId: string, data: any) => apiCache.threadMessages.set(threadId, data),
|
||||
|
||||
// Helper to clear parts of the cache when data changes
|
||||
invalidateThreadMessages: (threadId: string) => apiCache.threadMessages.delete(threadId),
|
||||
|
||||
// Functions to clear all cache
|
||||
clearAll: () => {
|
||||
apiCache.projects.clear();
|
||||
apiCache.threads.clear();
|
||||
apiCache.threadMessages.clear();
|
||||
console.log('[API] Cache cleared');
|
||||
},
|
||||
|
||||
clearProjects: () => {
|
||||
apiCache.projects.clear();
|
||||
console.log('[API] Projects cache cleared');
|
||||
},
|
||||
|
||||
clearThreads: () => {
|
||||
apiCache.threads.clear();
|
||||
console.log('[API] Threads cache cleared');
|
||||
}
|
||||
};
|
||||
|
||||
// Track active streams by agent run ID
|
||||
|
@ -36,20 +53,24 @@ export type Project = {
|
|||
description: string;
|
||||
account_id: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
sandbox: {
|
||||
vnc_preview?: string;
|
||||
sandbox_url?: string;
|
||||
id?: string;
|
||||
pass?: string;
|
||||
};
|
||||
[key: string]: any; // Allow additional properties to handle database fields
|
||||
}
|
||||
|
||||
export type Thread = {
|
||||
thread_id: string;
|
||||
account_id: string | null;
|
||||
project_id?: string | null;
|
||||
is_public?: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
[key: string]: any; // Allow additional properties to handle database fields
|
||||
}
|
||||
|
||||
export type Message = {
|
||||
|
@ -78,6 +99,7 @@ export const getProjects = async (): Promise<Project[]> => {
|
|||
// Check cache first
|
||||
const cached = apiCache.getProjects();
|
||||
if (cached) {
|
||||
console.log('[API] Returning cached projects:', cached.length);
|
||||
return cached;
|
||||
}
|
||||
|
||||
|
@ -96,9 +118,24 @@ export const getProjects = async (): Promise<Project[]> => {
|
|||
throw error;
|
||||
}
|
||||
|
||||
console.log('[API] Raw projects from DB:', data?.length, data);
|
||||
|
||||
// Map database fields to our Project type
|
||||
const mappedProjects: Project[] = (data || []).map(project => ({
|
||||
id: project.project_id,
|
||||
name: project.name || '',
|
||||
description: project.description || '',
|
||||
account_id: project.account_id,
|
||||
created_at: project.created_at,
|
||||
updated_at: project.updated_at,
|
||||
sandbox: project.sandbox || { id: "", pass: "", vnc_preview: "", sandbox_url: "" }
|
||||
}));
|
||||
|
||||
console.log('[API] Mapped projects for frontend:', mappedProjects.length);
|
||||
|
||||
// Cache the result
|
||||
apiCache.setProjects(data || []);
|
||||
return data || [];
|
||||
apiCache.setProjects(mappedProjects);
|
||||
return mappedProjects;
|
||||
} catch (err) {
|
||||
console.error('Error fetching projects:', err);
|
||||
// Return empty array for permission errors to avoid crashing the UI
|
||||
|
@ -122,6 +159,8 @@ export const getProject = async (projectId: string): Promise<Project> => {
|
|||
|
||||
if (error) throw error;
|
||||
|
||||
console.log('Raw project data from database:', data);
|
||||
|
||||
// If project has a sandbox, ensure it's started
|
||||
if (data.sandbox?.id) {
|
||||
try {
|
||||
|
@ -149,9 +188,21 @@ export const getProject = async (projectId: string): Promise<Project> => {
|
|||
}
|
||||
}
|
||||
|
||||
// Map database fields to our Project type
|
||||
const mappedProject: Project = {
|
||||
id: data.project_id,
|
||||
name: data.name || '',
|
||||
description: data.description || '',
|
||||
account_id: data.account_id,
|
||||
created_at: data.created_at,
|
||||
sandbox: data.sandbox || { id: "", pass: "", vnc_preview: "", sandbox_url: "" }
|
||||
};
|
||||
|
||||
console.log('Mapped project data for frontend:', mappedProject);
|
||||
|
||||
// Cache the result
|
||||
apiCache.setProject(projectId, data);
|
||||
return data;
|
||||
apiCache.setProject(projectId, mappedProject);
|
||||
return mappedProject;
|
||||
};
|
||||
|
||||
export const createProject = async (
|
||||
|
@ -196,6 +247,16 @@ export const createProject = async (
|
|||
|
||||
export const updateProject = async (projectId: string, data: Partial<Project>): Promise<Project> => {
|
||||
const supabase = createClient();
|
||||
|
||||
console.log('Updating project with ID:', projectId);
|
||||
console.log('Update data:', data);
|
||||
|
||||
// Sanity check to avoid update errors
|
||||
if (!projectId || projectId === '') {
|
||||
console.error('Attempted to update project with invalid ID:', projectId);
|
||||
throw new Error('Cannot update project: Invalid project ID');
|
||||
}
|
||||
|
||||
const { data: updatedData, error } = await supabase
|
||||
.from('projects')
|
||||
.update(data)
|
||||
|
@ -230,14 +291,14 @@ export const updateProject = async (projectId: string, data: Partial<Project>):
|
|||
}));
|
||||
}
|
||||
|
||||
// Return formatted project data
|
||||
// Return formatted project data - use same mapping as getProject
|
||||
return {
|
||||
id: updatedData.project_id,
|
||||
name: updatedData.name,
|
||||
description: updatedData.description || '',
|
||||
account_id: updatedData.account_id,
|
||||
created_at: updatedData.created_at,
|
||||
sandbox: updatedData.sandbox || { id: "", pass: "", vnc_preview: "" }
|
||||
sandbox: updatedData.sandbox || { id: "", pass: "", vnc_preview: "", sandbox_url: "" }
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -256,6 +317,7 @@ export const getThreads = async (projectId?: string): Promise<Thread[]> => {
|
|||
// Check cache first
|
||||
const cached = apiCache.getThreads(projectId || 'all');
|
||||
if (cached) {
|
||||
console.log('[API] Returning cached threads:', cached.length, projectId ? `for project ${projectId}` : 'for all projects');
|
||||
return cached;
|
||||
}
|
||||
|
||||
|
@ -263,16 +325,31 @@ export const getThreads = async (projectId?: string): Promise<Thread[]> => {
|
|||
let query = supabase.from('threads').select('*');
|
||||
|
||||
if (projectId) {
|
||||
console.log('[API] Filtering threads by project_id:', projectId);
|
||||
query = query.eq('project_id', projectId);
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
console.error('[API] Error fetching threads:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log('[API] Raw threads from DB:', data?.length, data);
|
||||
|
||||
// Map database fields to ensure consistency with our Thread type
|
||||
const mappedThreads: Thread[] = (data || []).map(thread => ({
|
||||
thread_id: thread.thread_id,
|
||||
account_id: thread.account_id,
|
||||
project_id: thread.project_id,
|
||||
created_at: thread.created_at,
|
||||
updated_at: thread.updated_at
|
||||
}));
|
||||
|
||||
// Cache the result
|
||||
apiCache.setThreads(projectId || 'all', data || []);
|
||||
return data || [];
|
||||
apiCache.setThreads(projectId || 'all', mappedThreads);
|
||||
return mappedThreads;
|
||||
};
|
||||
|
||||
export const getThread = async (threadId: string): Promise<Thread> => {
|
||||
|
@ -921,3 +998,40 @@ export const getSandboxFileContent = async (sandboxId: string, path: string): Pr
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Function to clear all API cache
|
||||
export const clearApiCache = () => {
|
||||
apiCache.clearAll();
|
||||
};
|
||||
|
||||
export const updateThread = async (threadId: string, data: Partial<Thread>): Promise<Thread> => {
|
||||
const supabase = createClient();
|
||||
|
||||
// Format the data for update
|
||||
const updateData = { ...data };
|
||||
|
||||
// Update the thread
|
||||
const { data: updatedThread, error } = await supabase
|
||||
.from('threads')
|
||||
.update(updateData)
|
||||
.eq('thread_id', threadId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating thread:', error);
|
||||
throw new Error(`Error updating thread: ${error.message}`);
|
||||
}
|
||||
|
||||
// Invalidate thread cache if we're updating thread data
|
||||
if (updatedThread.project_id) {
|
||||
apiCache.invalidateThreads(updatedThread.project_id);
|
||||
}
|
||||
apiCache.invalidateThreads('all');
|
||||
|
||||
return updatedThread;
|
||||
};
|
||||
|
||||
export const toggleThreadPublicStatus = async (threadId: string, isPublic: boolean): Promise<Thread> => {
|
||||
return updateThread(threadId, { is_public: isPublic });
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue