public project and file browsing

This commit is contained in:
Adam Cohen Hillel 2025-04-21 14:58:58 +01:00
parent 24d95d278c
commit bd0db22966
4 changed files with 56 additions and 36 deletions

View File

@ -53,6 +53,10 @@ async def verify_sandbox_access(client, sandbox_id: str, user_id: str):
raise HTTPException(status_code=404, detail="Sandbox not found") raise HTTPException(status_code=404, detail="Sandbox not found")
project_data = project_result.data[0] project_data = project_result.data[0]
if project_data.get('is_public'):
return project_data
account_id = project_data.get('account_id') account_id = project_data.get('account_id')
# Verify account membership # Verify account membership

View File

@ -6,6 +6,7 @@ CREATE TABLE projects (
description TEXT, description TEXT,
account_id UUID NOT NULL REFERENCES basejump.accounts(id) ON DELETE CASCADE, account_id UUID NOT NULL REFERENCES basejump.accounts(id) ON DELETE CASCADE,
sandbox JSONB DEFAULT '{}'::jsonb, sandbox JSONB DEFAULT '{}'::jsonb,
is_public BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL, 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 updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
); );
@ -96,7 +97,10 @@ ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Project policies -- Project policies
CREATE POLICY project_select_policy ON projects CREATE POLICY project_select_policy ON projects
FOR SELECT FOR SELECT
USING (basejump.has_role_on_account(account_id) = true); USING (
is_public = TRUE OR
basejump.has_role_on_account(account_id) = true OR
);
CREATE POLICY project_insert_policy ON projects CREATE POLICY project_insert_policy ON projects
FOR INSERT FOR INSERT
@ -274,6 +278,7 @@ CREATE POLICY message_delete_policy ON messages
-- Grant permissions to roles -- Grant permissions to roles
GRANT ALL PRIVILEGES ON TABLE projects TO authenticated, service_role; GRANT ALL PRIVILEGES ON TABLE projects TO authenticated, service_role;
GRANT SELECT ON TABLE projects TO anon;
GRANT SELECT ON TABLE threads TO authenticated, anon, service_role; GRANT SELECT ON TABLE threads TO authenticated, anon, service_role;
GRANT SELECT ON TABLE messages TO authenticated, anon, service_role; GRANT SELECT ON TABLE messages TO authenticated, anon, service_role;
GRANT ALL PRIVILEGES ON TABLE agent_runs TO authenticated, service_role; GRANT ALL PRIVILEGES ON TABLE agent_runs TO authenticated, service_role;

View File

@ -95,6 +95,8 @@ function renderMarkdownContent(content: string, handleToolClick: (assistantMessa
let lastIndex = 0; let lastIndex = 0;
const contentParts: React.ReactNode[] = []; const contentParts: React.ReactNode[] = [];
let match; let match;
// Generate a unique timestamp for this render to avoid key conflicts
const timestamp = Date.now();
// If no XML tags found, just return the full content as markdown // If no XML tags found, just return the full content as markdown
if (!content.match(xmlRegex)) { if (!content.match(xmlRegex)) {
@ -106,7 +108,7 @@ function renderMarkdownContent(content: string, handleToolClick: (assistantMessa
if (match.index > lastIndex) { if (match.index > lastIndex) {
const textBeforeTag = content.substring(lastIndex, match.index); const textBeforeTag = content.substring(lastIndex, match.index);
contentParts.push( contentParts.push(
<Markdown key={`md-${lastIndex}`} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none inline-block mr-1">{textBeforeTag}</Markdown> <Markdown key={`md-${lastIndex}-${timestamp}`} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none inline-block mr-1">{textBeforeTag}</Markdown>
); );
} }
@ -114,7 +116,7 @@ function renderMarkdownContent(content: string, handleToolClick: (assistantMessa
const toolName = match[1] || match[2]; const toolName = match[1] || match[2];
const IconComponent = getToolIcon(toolName); const IconComponent = getToolIcon(toolName);
const paramDisplay = extractPrimaryParam(toolName, rawXml); const paramDisplay = extractPrimaryParam(toolName, rawXml);
const toolCallKey = `tool-${match.index}`; const toolCallKey = `tool-${match.index}-${timestamp}`;
if (toolName === 'ask') { if (toolName === 'ask') {
// Extract attachments from the XML attributes // Extract attachments from the XML attributes
@ -129,7 +131,7 @@ function renderMarkdownContent(content: string, handleToolClick: (assistantMessa
// Render <ask> tag content with attachment UI // Render <ask> tag content with attachment UI
contentParts.push( contentParts.push(
<div key={`ask-${match.index}`} className="space-y-3"> <div key={`ask-${match.index}-${timestamp}`} className="space-y-3">
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3">{askContent}</Markdown> <Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3">{askContent}</Markdown>
{attachments.length > 0 && ( {attachments.length > 0 && (
@ -151,7 +153,7 @@ function renderMarkdownContent(content: string, handleToolClick: (assistantMessa
return ( return (
<button <button
key={`attachment-${idx}`} key={`attachment-${idx}-${timestamp}`}
onClick={() => fileViewerHandler?.(attachment)} onClick={() => fileViewerHandler?.(attachment)}
className="group inline-flex items-center gap-2 rounded-md border bg-muted/5 px-2.5 py-1.5 text-sm transition-colors hover:bg-muted/10" className="group inline-flex items-center gap-2 rounded-md border bg-muted/5 px-2.5 py-1.5 text-sm transition-colors hover:bg-muted/10"
> >
@ -192,7 +194,7 @@ function renderMarkdownContent(content: string, handleToolClick: (assistantMessa
// Add text after the last tag // Add text after the last tag
if (lastIndex < content.length) { if (lastIndex < content.length) {
contentParts.push( contentParts.push(
<Markdown key={`md-${lastIndex}`} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none">{content.substring(lastIndex)}</Markdown> <Markdown key={`md-${lastIndex}-${timestamp}`} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none">{content.substring(lastIndex)}</Markdown>
); );
} }
@ -294,10 +296,13 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
} }
setMessages(prev => { setMessages(prev => {
// First check if the message already exists
const messageExists = prev.some(m => m.message_id === message.message_id); const messageExists = prev.some(m => m.message_id === message.message_id);
if (messageExists) { if (messageExists) {
// If it exists, update it instead of adding a new one
return prev.map(m => m.message_id === message.message_id ? message : m); return prev.map(m => m.message_id === message.message_id ? message : m);
} else { } else {
// If it's a new message, add it to the end
return [...prev, message]; return [...prev, message];
} }
}); });
@ -1250,7 +1255,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
visibleMessages.forEach((message, index) => { visibleMessages.forEach((message, index) => {
const messageType = message.type; const messageType = message.type;
const key = message.message_id || `msg-${index}`; const key = message.message_id ? `${message.message_id}-${index}` : `msg-${index}`;
if (messageType === 'user') { if (messageType === 'user') {
if (currentGroup) { if (currentGroup) {
@ -1327,7 +1332,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
group.messages.forEach((message, msgIndex) => { group.messages.forEach((message, msgIndex) => {
if (message.type === 'assistant') { if (message.type === 'assistant') {
const parsedContent = safeJsonParse<ParsedContent>(message.content, {}); const parsedContent = safeJsonParse<ParsedContent>(message.content, {});
const msgKey = message.message_id || `submsg-assistant-${msgIndex}`; const msgKey = message.message_id ? `${message.message_id}-${msgIndex}` : `submsg-assistant-${msgIndex}`;
if (!parsedContent.content) return; if (!parsedContent.content) return;
@ -1483,6 +1488,27 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
> >
<ArrowDown className="h-4 w-4 rotate-90" /> <ArrowDown className="h-4 w-4 rotate-90" />
</Button> </Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
setIsPlaying(false);
setCurrentMessageIndex(messages.length);
setVisibleMessages(messages);
setToolPlaybackIndex(toolCalls.length - 1);
setStreamingText("");
setIsStreamingText(false);
setCurrentToolCall(null);
if (toolCalls.length > 0) {
setCurrentToolIndex(toolCalls.length - 1);
setIsSidePanelOpen(true);
}
}}
className="text-xs"
>
Skip to end
</Button>
</div> </div>
</div> </div>
)} )}
@ -1514,33 +1540,14 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
renderToolResult={toolViewResult} renderToolResult={toolViewResult}
/> />
{sandboxId && ( {/* Show FileViewerModal regardless of sandboxId availability */}
<FileViewerModal <FileViewerModal
open={fileViewerOpen} open={fileViewerOpen}
onOpenChange={setFileViewerOpen} onOpenChange={setFileViewerOpen}
sandboxId={sandboxId} sandboxId={sandboxId || ""}
initialFilePath={fileToView} initialFilePath={fileToView}
project={project} project={project}
/> />
)}
{/* Show a fallback modal when sandbox is not available */}
{!sandboxId && fileViewerOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-background rounded-lg p-6 max-w-md w-full">
<h3 className="text-lg font-medium mb-2">File Unavailable</h3>
<p className="text-sm text-muted-foreground mb-4">
The file viewer is not available for this shared thread.
</p>
<Button
onClick={() => setFileViewerOpen(false)}
className="w-full"
>
Close
</Button>
</div>
</div>
)}
</div> </div>
); );
} }

View File

@ -144,7 +144,11 @@ export function FileOperationToolView({
? `${project.sandbox.sandbox_url}/${processedFilePath}` ? `${project.sandbox.sandbox_url}/${processedFilePath}`
: undefined; : undefined;
console.log('HTML Preview URL:', htmlPreviewUrl); // Only log HTML preview URL when it exists
if (htmlPreviewUrl) {
console.log('HTML Preview URL:', htmlPreviewUrl);
}
// Add state for view mode toggle (code or preview) // Add state for view mode toggle (code or preview)
const [viewMode, setViewMode] = useState<'code' | 'preview'>(isHtml || isMarkdown || isCsv ? 'preview' : 'code'); const [viewMode, setViewMode] = useState<'code' | 'preview'>(isHtml || isMarkdown || isCsv ? 'preview' : 'code');