mirror of https://github.com/kortix-ai/suna.git
public project and file browsing
This commit is contained in:
parent
24d95d278c
commit
bd0db22966
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue