Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const TOOL_TO_RENDERER_CONFIG: ToolToRendererCondition[] = [
{ toolName: 'web_search', renderer: 'search_result' },
{ toolName: 'browser_vision_control', renderer: 'browser_vision_control' },
{ toolName: 'browser_screenshot', renderer: 'image' },
{ toolName: 'browser_get_markdown', renderer: 'browser_get_markdown' },
{ toolName: 'write_file', renderer: 'file_result' },
{ toolName: 'read_file', renderer: 'file_result' },
{ toolName: 'edit_file', renderer: 'diff_result' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getFileTypeInfo, getDefaultDisplayMode } from './utils/fileTypeUtils';
import { ImageRenderer } from './renderers/ImageRenderer';
import { LinkRenderer } from './renderers/LinkRenderer';
import { LinkReaderRenderer } from './renderers/LinkReaderRenderer';
import { BrowserGetMarkdownRenderer } from './renderers/BrowserGetMarkdownRenderer';
import { SearchResultRenderer } from './renderers/SearchResultRenderer';
import { CommandResultRenderer } from './renderers/CommandResultRenderer';
import { ScriptResultRenderer } from './renderers/ScriptResultRenderer';
Expand All @@ -41,6 +42,7 @@ const CONTENT_RENDERERS: Record<
image: ImageRenderer,
link: LinkRenderer,
link_reader: LinkReaderRenderer,
browser_get_markdown: BrowserGetMarkdownRenderer,
search_result: SearchResultRenderer,
command_result: CommandResultRenderer,
script_result: ScriptResultRenderer,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React from 'react';
import { StandardPanelContent } from '../types/panelContent';
import { MarkdownContentRenderer } from './MarkdownContentRenderer';
import { FileDisplayMode } from '../types';
import { isOmniTarsTextContentArray, OmniTarsTextContent } from '@/common/services/SearchService';

interface BrowserGetMarkdownRendererProps {
panelContent: StandardPanelContent;
onAction?: (action: string, data: unknown) => void;
displayMode?: FileDisplayMode;
}

interface BrowserMarkdownResult {
content: string;
title?: string;
pagination?: {
currentPage: number;
totalPages: number;
hasMorePages: boolean;
};
}

/**
* Renderer for browser_get_markdown tool output
* Converts browser markdown results to the format expected by MarkdownContentRenderer
*/
export const BrowserGetMarkdownRenderer: React.FC<BrowserGetMarkdownRendererProps> = ({
panelContent,
onAction,
}) => {
const markdownData = extractBrowserMarkdownData(panelContent);

if (!markdownData) {
return (
<div className="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400 text-sm">
No markdown content available
</div>
);
}

// Convert browser markdown data to MarkdownContentRenderer format
const items = [
{
url: getCurrentPageUrl(panelContent),
title: markdownData.title || 'Page Content',
content: markdownData.content,
},
];

return <MarkdownContentRenderer items={items} onAction={onAction} />;
};

/**
* Extract browser markdown data from panelContent
* Handles different response formats from browser_get_markdown tool
*/
function extractBrowserMarkdownData(panelContent: StandardPanelContent): BrowserMarkdownResult | null {
try {
let parsedData: BrowserMarkdownResult;

// Handle different data formats
if (typeof panelContent.source === 'object' && panelContent.source !== null) {
const sourceObj = panelContent.source as {
content: OmniTarsTextContent[];
structuredContent?: BrowserMarkdownResult;
};

// Check if structuredContent exists directly in source
if (sourceObj.structuredContent && typeof sourceObj.structuredContent === 'object') {
parsedData = sourceObj.structuredContent;
}
// Try content array with JSON text field
else if (isOmniTarsTextContentArray(sourceObj.content)) {
const textContent = sourceObj.content[0].text;

try {
parsedData = JSON.parse(textContent);
} catch {
// If JSON parsing fails, treat as plain text content
parsedData = {
content: textContent,
title: 'Page Content',
};
}
}
// Fallback
else {
return null;
}
} else if (typeof panelContent.source === 'string') {
try {
parsedData = JSON.parse(panelContent.source);
} catch {
// If JSON parsing fails, treat as plain text content
parsedData = {
content: panelContent.source,
title: 'Page Content',
};
}
} else {
return null;
}

// Validate that we have content
if (!parsedData?.content || typeof parsedData.content !== 'string') {
return null;
}

return parsedData;
} catch (error) {
console.warn('Failed to extract browser markdown data:', error);
return null;
}
}

/**
* Get current page URL from panel content arguments or fallback
*/
function getCurrentPageUrl(panelContent: StandardPanelContent): string {
// Try to get URL from arguments
if (panelContent.arguments?.url && typeof panelContent.arguments.url === 'string') {
return panelContent.arguments.url;
}

// Fallback to a generic browser page indicator
return 'browser://current-page';
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React, { useState } from 'react';
import { FiExternalLink, FiCopy, FiCheck, FiGlobe } from 'react-icons/fi';
import React from 'react';
import { StandardPanelContent } from '../types/panelContent';
import { MarkdownRenderer } from '@tarko/ui';
import { wrapMarkdown } from '@/common/utils/markdown';
import { MarkdownContentRenderer } from './MarkdownContentRenderer';
import { FileDisplayMode } from '../types';
import { isOmniTarsTextContentArray, OmniTarsTextContent } from '@/common/services/SearchService';

Expand Down Expand Up @@ -31,13 +29,13 @@ interface LinkReaderResponse {
}

/**
* Elegant and minimal LinkReader renderer
* Clean design with subtle interactions and refined typography
* LinkReader renderer using the generic MarkdownContentRenderer
* Converts LinkReader data to the format expected by MarkdownContentRenderer
*/
export const LinkReaderRenderer: React.FC<LinkReaderRendererProps> = ({ panelContent }) => {
const [copiedStates, setCopiedStates] = useState<boolean[]>([]);
const [showMarkdownSource, setShowMarkdownSource] = useState(true);

export const LinkReaderRenderer: React.FC<LinkReaderRendererProps> = ({
panelContent,
onAction
}) => {
const linkData = extractLinkReaderData(panelContent);

if (!linkData?.results?.length) {
Expand All @@ -48,96 +46,14 @@ export const LinkReaderRenderer: React.FC<LinkReaderRendererProps> = ({ panelCon
);
}

const copyContent = async (content: string, index: number) => {
try {
await navigator.clipboard.writeText(content);
setCopiedStates((prevStates) => {
const newStates = [...prevStates];
newStates[index] = true;
return newStates;
});
setTimeout(() => {
setCopiedStates((prevStates) => {
const newStates = [...prevStates];
newStates[index] = false;
return newStates;
});
}, 1500);
} catch (error) {
console.error('Copy failed:', error);
}
};

return (
<div className="space-y-3">
{linkData.results.map((result, index) => {
const isCopied = copiedStates[index];

return (
<div
key={`link-${index}`}
className="group relative rounded-xl border border-gray-800 transition-all duration-300 hover:border-gray-700 hover:shadow-lg hover:shadow-gray-900/20"
style={{ backgroundColor: '#111111' }}
>
{/* Floating copy button */}
<button
onClick={() => copyContent(result.content, index)}
className={`absolute top-6 right-6 z-10 p-2 rounded-lg backdrop-blur-md transition-all duration-200 opacity-0 group-hover:opacity-100 ${
isCopied
? 'bg-green-900/40 text-green-400 border border-green-700/50'
: 'bg-gray-800/80 text-gray-400 border border-gray-600/50 hover:bg-gray-700 hover:text-gray-300'
}`}
title="Copy content"
>
{isCopied ? (
<FiCheck size={14} className="transition-transform scale-110" />
) : (
<FiCopy size={14} />
)}
</button>

{/* Content container */}
<div className="p-2">
{/* Elegant header */}
<div className="flex items-start gap-3 m-4 mb-0">
<div className="flex-shrink-0 w-8 h-8 bg-gradient-to-br from-purple-400/20 to-violet-400/20 rounded-lg flex items-center justify-center border border-purple-600/40 shadow-sm">
<FiGlobe size={16} className="text-purple-300" />
</div>

<div className="flex-1 min-w-0">
<h3 className="text-base font-semibold text-gray-100 leading-snug mb-1 line-clamp-2">
{result.title}
</h3>

<a
href={result.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-purple-400/80 hover:text-purple-300 transition-colors group/link font-medium"
>
<span className="truncate max-w-sm">{formatUrl(result.url)}</span>
<FiExternalLink
size={12}
className="flex-shrink-0 opacity-70 group-hover/link:opacity-100 group-hover/link:translate-x-0.5 transition-all duration-200"
/>
</a>
</div>
</div>

{/* Content area */}
<div>
<MarkdownRenderer
content={wrapMarkdown(result.content)}
forceDarkTheme
codeBlockStyle={{ whiteSpace: 'pre-wrap' }}
/>
</div>
</div>
</div>
);
})}
</div>
);
// Convert LinkReader results to MarkdownContentRenderer format
const items = linkData.results.map((result) => ({
url: result.url,
title: result.title,
content: result.content,
}));

return <MarkdownContentRenderer items={items} onAction={onAction} />;
};

/**
Expand Down Expand Up @@ -389,22 +305,4 @@ function getHostname(url: string): string {
}
}

function formatUrl(url: string): string {
try {
const urlObj = new URL(url);
const hostname = urlObj.hostname.replace(/^www\./, '');
const path = urlObj.pathname;

if (path === '/' || path === '') {
return hostname;
}

if (path.length > 25) {
return `${hostname}${path.substring(0, 20)}...`;
}

return `${hostname}${path}`;
} catch {
return url;
}
}
Loading
Loading