u
This commit is contained in:
@@ -31,10 +31,11 @@ import { vfs as vfsApi } from '../../api';
|
||||
export default class VfsAppletRunner extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { inputSchema, outputColumns } = parseAppletSchemas(props.content || '');
|
||||
const { inputSchema, outputColumns, description } = parseAppletSchemas(props.content || '');
|
||||
this.state = {
|
||||
inputSchema,
|
||||
outputColumns,
|
||||
description: description || props.file?.description || '',
|
||||
inputValues: buildAppletDefaults(inputSchema),
|
||||
isRunning: false,
|
||||
runError: null,
|
||||
@@ -50,10 +51,11 @@ export default class VfsAppletRunner extends PureComponent {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.content !== this.props.content || prevProps.fileName !== this.props.fileName) {
|
||||
const { inputSchema, outputColumns } = parseAppletSchemas(this.props.content || '');
|
||||
const { inputSchema, outputColumns, description } = parseAppletSchemas(this.props.content || '');
|
||||
this.setState({
|
||||
inputSchema,
|
||||
outputColumns,
|
||||
description: description || this.props.file?.description || '',
|
||||
inputValues: buildAppletDefaults(inputSchema),
|
||||
isRunning: false,
|
||||
runError: null,
|
||||
@@ -122,6 +124,7 @@ export default class VfsAppletRunner extends PureComponent {
|
||||
stdout: res?.stdout || '',
|
||||
durationMs: res?.durationMs ?? null,
|
||||
truncated: Boolean(res?.truncated),
|
||||
description: res?.description || this.state.description,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
@@ -236,6 +239,7 @@ export default class VfsAppletRunner extends PureComponent {
|
||||
const {
|
||||
inputSchema,
|
||||
outputColumns,
|
||||
description: stateDescription,
|
||||
inputValues,
|
||||
isRunning,
|
||||
runError,
|
||||
@@ -245,6 +249,7 @@ export default class VfsAppletRunner extends PureComponent {
|
||||
durationMs,
|
||||
truncated,
|
||||
} = this.state;
|
||||
const description = stateDescription || file?.description;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
@@ -268,14 +273,27 @@ export default class VfsAppletRunner extends PureComponent {
|
||||
<ArrowBackRoundedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<VfsFileIcon fileName={fileName} mime={file?.mime} fontSize="small" />
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={700}
|
||||
noWrap
|
||||
sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}
|
||||
>
|
||||
{fileName}
|
||||
</Typography>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={700}
|
||||
noWrap
|
||||
sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}
|
||||
>
|
||||
{fileName}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
noWrap
|
||||
title={description}
|
||||
sx={{ display: 'block', fontSize: '0.72rem', lineHeight: 1.2 }}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexShrink: 0 }}>
|
||||
|
||||
@@ -55,7 +55,11 @@ function buildFileTree(files, searchQuery = '') {
|
||||
const q = searchQuery ? searchQuery.trim().toLowerCase() : '';
|
||||
const filtered = (files || []).filter((f) => {
|
||||
if (!q) return true;
|
||||
return f.name.toLowerCase().includes(q) || (f.mime && f.mime.toLowerCase().includes(q));
|
||||
return (
|
||||
f.name.toLowerCase().includes(q) ||
|
||||
(f.mime && f.mime.toLowerCase().includes(q)) ||
|
||||
(f.description && f.description.toLowerCase().includes(q))
|
||||
);
|
||||
});
|
||||
|
||||
const root = {
|
||||
@@ -457,25 +461,65 @@ export default class VfsFileExplorer extends PureComponent {
|
||||
/>
|
||||
|
||||
<Box sx={{ minWidth: 0, flexGrow: 1, pr: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={isSelected ? 600 : 500}
|
||||
noWrap
|
||||
sx={{ fontFamily: 'monospace', fontSize: '0.82rem' }}
|
||||
>
|
||||
{file.name}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, mt: 0.15 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
||||
{formatVfsSize(file.fileData.size)}
|
||||
</Typography>
|
||||
{file.fileData.updated_at && (
|
||||
<Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.68rem' }}>
|
||||
{formatVfsDate(file.fileData.updated_at)}
|
||||
{file.fileData?.description ? (
|
||||
<>
|
||||
{/* Line 1: Description */}
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={isSelected ? 600 : 500}
|
||||
noWrap
|
||||
title={file.fileData.description}
|
||||
sx={{ fontSize: '0.82rem', lineHeight: 1.3 }}
|
||||
>
|
||||
{file.fileData.description}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Line 2: size, date, filename */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.15, minWidth: 0 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem', flexShrink: 0 }}>
|
||||
{formatVfsSize(file.fileData.size)}
|
||||
</Typography>
|
||||
{file.fileData.updated_at && (
|
||||
<Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.68rem', flexShrink: 0 }}>
|
||||
{formatVfsDate(file.fileData.updated_at)}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
noWrap
|
||||
title={file.name}
|
||||
sx={{ fontFamily: 'monospace', fontSize: '0.68rem', minWidth: 0 }}
|
||||
>
|
||||
{file.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Line 1: filename */}
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={isSelected ? 600 : 500}
|
||||
noWrap
|
||||
sx={{ fontFamily: 'monospace', fontSize: '0.82rem' }}
|
||||
>
|
||||
{file.name}
|
||||
</Typography>
|
||||
|
||||
{/* Line 2: size, date */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, mt: 0.15 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
||||
{formatVfsSize(file.fileData.size)}
|
||||
</Typography>
|
||||
{file.fileData.updated_at && (
|
||||
<Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.68rem' }}>
|
||||
{formatVfsDate(file.fileData.updated_at)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Quick action buttons on hover / select */}
|
||||
|
||||
@@ -208,15 +208,38 @@ export function parseCsvRows(text, separator = APPLET_CSV_SEPARATOR) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the applet description header.
|
||||
*
|
||||
* Accepts either a quoted string (e.g. `"Lorem ipsum"` or `'Lorem ipsum'`) or bare text.
|
||||
* Strips surrounding quotes if present and trims whitespace.
|
||||
*
|
||||
* @param {string} header Raw text after the `// @applet-description ` marker.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parseAppletDescription(header) {
|
||||
const text = String(header || '').trim();
|
||||
if (!text) return '';
|
||||
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (typeof parsed === 'string' && parsed.trim()) return parsed.trim();
|
||||
} catch {
|
||||
const unquoted = text.slice(1, -1).trim();
|
||||
if (unquoted) return unquoted;
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse applet metadata from the file header.
|
||||
*
|
||||
* Mirrors the backend's `readApplet()`: the input JSON Schema must be on line 1 and the typed
|
||||
* CSV output columns on line 2. Parsing the same two lines keeps the UI in lockstep with what
|
||||
* the sandbox will actually accept.
|
||||
* Mirrors the backend's `readApplet()`: the input JSON Schema must be on line 1, the typed
|
||||
* CSV output columns on line 2, and the applet description on line 3.
|
||||
*
|
||||
* @param {string} content Applet file source.
|
||||
* @returns {{ inputSchema: object|null, outputColumns: Array<{name: string, type: string}>|null }}
|
||||
* @returns {{ inputSchema: object|null, outputColumns: Array<{name: string, type: string}>|null, description: string }}
|
||||
*/
|
||||
export function parseAppletSchemas(content) {
|
||||
const lines = String(content || '').replace(/^\uFEFF/, '').split(/\r?\n/);
|
||||
@@ -238,7 +261,15 @@ export function parseAppletSchemas(content) {
|
||||
outputColumns = parseTypedCsvColumns(outputMatch[1]);
|
||||
}
|
||||
|
||||
return { inputSchema, outputColumns };
|
||||
let description = '';
|
||||
const descMatch =
|
||||
lines[2]?.match(/^\/\/ @applet-description (.+)$/) ||
|
||||
lines.slice(0, 10).map((l) => l.match(/^\/\/ @applet-description (.+)$/)).find(Boolean);
|
||||
if (descMatch) {
|
||||
description = parseAppletDescription(descMatch[1]);
|
||||
}
|
||||
|
||||
return { inputSchema, outputColumns, description };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user