This commit is contained in:
marko-kraemer 2025-04-20 17:08:17 +01:00 committed by Adam Cohen Hillel
parent a4f01b6092
commit 797b496863
17 changed files with 1992 additions and 186 deletions

View File

@ -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

View File

@ -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>

View File

@ -1,6 +1,6 @@
"use client"
import { SidebarLeft } from "@/components/dashboard/sidebar/sidebar-left"
import { SidebarLeft } from "@/components/sidebar/sidebar-left"
import {
SidebarInset,
SidebarProvider,

View File

@ -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

View File

@ -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>
))}
</>
)
}

View File

@ -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 (

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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)

View File

@ -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 });
};