u
This commit is contained in:
@@ -7,7 +7,7 @@ description: >-
|
|||||||
|
|
||||||
# Dash API Routes Overview (All)
|
# Dash API Routes Overview (All)
|
||||||
|
|
||||||
Overview of all 206 API endpoints across all modules.
|
Overview of all 207 API endpoints across all modules.
|
||||||
|
|
||||||
### Notation
|
### Notation
|
||||||
|
|
||||||
@@ -689,6 +689,9 @@ _none_
|
|||||||
#### Get a virtual filesystem file — `GET /vfs/{*path}`
|
#### Get a virtual filesystem file — `GET /vfs/{*path}`
|
||||||
- Params: `path: str, query: download?: bool, raw?: bool`
|
- Params: `path: str, query: download?: bool, raw?: bool`
|
||||||
- Returns: `200 str`
|
- Returns: `200 str`
|
||||||
|
#### Execute a virtual filesystem applet — `POST /vfs/{*path}`
|
||||||
|
- Params: `path: str, body?: { input?: {}, timeout?: int, max_output?: int }`
|
||||||
|
- Returns: `200 { ok: bool, result: any|null, stdout: str, consoleLines: int, durationMs: int, truncated?: bool, truncationNote?: str }`
|
||||||
#### Create or overwrite a virtual filesystem file — `PUT /vfs/{*path}`
|
#### Create or overwrite a virtual filesystem file — `PUT /vfs/{*path}`
|
||||||
- Params: `path: str, body: { content: str, mime?: str }`
|
- Params: `path: str, body: { content: str, mime?: str }`
|
||||||
- Returns: `200 { success: bool, file: { name: str, size: int, mime: str, updated_at: str } }`
|
- Returns: `200 { success: bool, file: { name: str, size: int, mime: str, updated_at: str } }`
|
||||||
|
|||||||
157
src/components/vfs/VfsAppletForm.jsx
Normal file
157
src/components/vfs/VfsAppletForm.jsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Switch from '@mui/material/Switch';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file VfsAppletForm.jsx
|
||||||
|
* @description Schema-driven form renderer for applet input/output schemas.
|
||||||
|
* Renders one field per `schema.properties` entry. Supports string, number,
|
||||||
|
* integer, boolean, array and object types. Array/object values are edited
|
||||||
|
* as JSON text. In `readOnly` mode all fields are disabled (result preview).
|
||||||
|
*/
|
||||||
|
export default class VfsAppletForm extends PureComponent {
|
||||||
|
handleFieldChange = (key, nextVal) => {
|
||||||
|
const { values, onChange } = this.props;
|
||||||
|
if (!onChange) return;
|
||||||
|
onChange({ ...(values || {}), [key]: nextVal });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleJsonTextChange = (key, type, rawText) => {
|
||||||
|
// Try to keep parsed JSON in state; fall back to raw text while typing.
|
||||||
|
const trimmed = String(rawText || '').trim();
|
||||||
|
if (trimmed === '') {
|
||||||
|
this.handleFieldChange(key, type === 'array' ? [] : {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.handleFieldChange(key, JSON.parse(rawText));
|
||||||
|
} catch {
|
||||||
|
this.handleFieldChange(key, rawText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderField = (key, prop) => {
|
||||||
|
const { values, readOnly } = this.props;
|
||||||
|
const raw = values?.[key];
|
||||||
|
const type = prop?.type || 'string';
|
||||||
|
const title = prop?.title || key;
|
||||||
|
const required = Array.isArray(this.props.schema?.required)
|
||||||
|
&& this.props.schema.required.includes(key);
|
||||||
|
const label = required ? `${title} *` : title;
|
||||||
|
|
||||||
|
if (type === 'boolean') {
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
key={key}
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={Boolean(raw)}
|
||||||
|
disabled={Boolean(readOnly)}
|
||||||
|
onChange={(e) => this.handleFieldChange(key, e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" fontWeight={600} sx={{ fontSize: '0.82rem' }}>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
{prop?.description && (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{prop.description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
sx={{ m: 0, py: 0.25 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'number' || type === 'integer') {
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
key={key}
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
type="number"
|
||||||
|
label={label}
|
||||||
|
value={raw === undefined || raw === null ? '' : raw}
|
||||||
|
disabled={Boolean(readOnly)}
|
||||||
|
helperText={prop?.description || (prop?.default !== undefined ? `Standard: ${String(prop.default)}` : '')}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value;
|
||||||
|
if (v === '') {
|
||||||
|
this.handleFieldChange(key, '');
|
||||||
|
} else {
|
||||||
|
const num = Number(v);
|
||||||
|
this.handleFieldChange(key, Number.isNaN(num) ? v : num);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{ '& .MuiInputBase-input': { fontSize: '0.85rem' } }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'array' || type === 'object') {
|
||||||
|
const text = typeof raw === 'string' ? raw : JSON.stringify(raw ?? (type === 'array' ? [] : {}), null, 2);
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
key={key}
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
maxRows={8}
|
||||||
|
label={`${label} (JSON)`}
|
||||||
|
value={text}
|
||||||
|
disabled={Boolean(readOnly)}
|
||||||
|
helperText={prop?.description || ''}
|
||||||
|
onChange={(e) => this.handleJsonTextChange(key, type, e.target.value)}
|
||||||
|
sx={{
|
||||||
|
'& .MuiInputBase-input': { fontFamily: 'monospace', fontSize: '0.78rem' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: string (also covers enum -> plain text for now)
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
key={key}
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
label={label}
|
||||||
|
value={raw === undefined || raw === null ? '' : String(raw)}
|
||||||
|
disabled={Boolean(readOnly)}
|
||||||
|
helperText={prop?.description || (prop?.default !== undefined ? `Standard: ${String(prop.default)}` : '')}
|
||||||
|
onChange={(e) => this.handleFieldChange(key, e.target.value)}
|
||||||
|
inputProps={{ autoComplete: 'off', autoCorrect: 'off', autoCapitalize: 'off', spellCheck: false }}
|
||||||
|
sx={{ '& .MuiInputBase-input': { fontSize: '0.85rem' } }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { schema, emptyHint } = this.props;
|
||||||
|
const props = schema?.properties;
|
||||||
|
const keys = props && typeof props === 'object' ? Object.keys(props) : [];
|
||||||
|
|
||||||
|
if (keys.length === 0) {
|
||||||
|
return (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.8rem' }}>
|
||||||
|
{emptyHint || 'Keine Felder im Schema definiert.'}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||||
|
{keys.map((key) => this.renderField(key, props[key]))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
196
src/components/vfs/VfsAppletRunner.jsx
Normal file
196
src/components/vfs/VfsAppletRunner.jsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||||
|
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||||
|
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
|
||||||
|
import CodeRoundedIcon from '@mui/icons-material/CodeRounded';
|
||||||
|
import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
|
||||||
|
import InputRoundedIcon from '@mui/icons-material/InputRounded';
|
||||||
|
import OutputRoundedIcon from '@mui/icons-material/OutputRounded';
|
||||||
|
|
||||||
|
import VfsFileIcon from './VfsFileIcon';
|
||||||
|
import VfsAppletForm from './VfsAppletForm';
|
||||||
|
import { parseAppletSchemas, buildAppletDefaults } from './vfsUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file VfsAppletRunner.jsx
|
||||||
|
* @description UI shell for `*.apple.js` applet files (forms-only, no execution yet).
|
||||||
|
* Shows input form (editable) and output form (read-only) parsed from
|
||||||
|
* `// @applet-input-schema {...}` / `// @applet-output-schema {...}` markers,
|
||||||
|
* plus a Run button that intentionally does nothing for now.
|
||||||
|
*/
|
||||||
|
export default class VfsAppletRunner extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const { inputSchema, outputSchema } = parseAppletSchemas(props.content || '');
|
||||||
|
this.state = {
|
||||||
|
inputSchema,
|
||||||
|
outputSchema,
|
||||||
|
inputValues: buildAppletDefaults(inputSchema),
|
||||||
|
// Output preview starts with schema defaults (read-only, no execution yet).
|
||||||
|
outputValues: buildAppletDefaults(outputSchema),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.content !== this.props.content || prevProps.fileName !== this.props.fileName) {
|
||||||
|
const { inputSchema, outputSchema } = parseAppletSchemas(this.props.content || '');
|
||||||
|
this.setState({
|
||||||
|
inputSchema,
|
||||||
|
outputSchema,
|
||||||
|
inputValues: buildAppletDefaults(inputSchema),
|
||||||
|
outputValues: buildAppletDefaults(outputSchema),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange = (nextValues) => {
|
||||||
|
this.setState({ inputValues: nextValues });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Intentionally a no-op: execution will be wired up in a later step.
|
||||||
|
handleRun = () => {};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { fileName, file, loadingContent, onClose, onShowCode, onMentionInChat } = this.props;
|
||||||
|
const { inputSchema, outputSchema, inputValues, outputValues } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||||
|
{/* Runner Header */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 1.25,
|
||||||
|
px: 2,
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
flexShrink: 0,
|
||||||
|
minHeight: 48,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, minWidth: 0, mr: 1 }}>
|
||||||
|
<IconButton size="small" onClick={onClose} aria-label="Zurück zur Dateiliste" sx={{ p: 0.5, mr: 0.25 }}>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexShrink: 0 }}>
|
||||||
|
{onShowCode && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<CodeRoundedIcon />}
|
||||||
|
onClick={onShowCode}
|
||||||
|
sx={{ borderRadius: 2, fontSize: '0.78rem', fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
Code
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onMentionInChat && (
|
||||||
|
<IconButton size="small" onClick={onMentionInChat} aria-label="In Chat erwähnen" sx={{ p: 0.6 }}>
|
||||||
|
<ChatBubbleOutlineRoundedIcon fontSize="small" color="primary" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<IconButton size="small" onClick={onClose} aria-label="Runner schließen" sx={{ p: 0.6, ml: 0.5 }}>
|
||||||
|
<CloseRoundedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Runner Content */}
|
||||||
|
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
{loadingContent ? (
|
||||||
|
<Box sx={{ flexGrow: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1.5, color: 'text.secondary' }}>
|
||||||
|
<CircularProgress size={24} />
|
||||||
|
<Typography variant="body2">Lade Applet…</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Input Section */}
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
||||||
|
<InputRoundedIcon color="primary" sx={{ fontSize: 20 }} />
|
||||||
|
<Typography variant="subtitle2" fontWeight={700}>
|
||||||
|
Eingabe
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<VfsAppletForm
|
||||||
|
schema={inputSchema}
|
||||||
|
values={inputValues}
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
emptyHint="Kein @applet-input-schema im Dateikopf gefunden."
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Run Button (no-op for now) */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<PlayArrowRoundedIcon />}
|
||||||
|
onClick={this.handleRun}
|
||||||
|
title="Ausführung folgt in einem späteren Schritt"
|
||||||
|
sx={{ borderRadius: 2, fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
Ausführen
|
||||||
|
</Button>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Führt das Applet noch nicht aus (nur Formular-Vorschau).
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Output Section (read-only preview) */}
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
||||||
|
<OutputRoundedIcon color="action" sx={{ fontSize: 20 }} />
|
||||||
|
<Typography variant="subtitle2" fontWeight={700}>
|
||||||
|
Ergebnis
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<VfsAppletForm
|
||||||
|
schema={outputSchema}
|
||||||
|
values={outputValues}
|
||||||
|
readOnly
|
||||||
|
emptyHint="Kein @applet-output-schema im Dateikopf gefunden."
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{!inputSchema && !outputSchema && (
|
||||||
|
<Alert severity="info" sx={{ fontSize: '0.8rem' }}>
|
||||||
|
In dieser Datei wurden keine Applet-Schemas gefunden. Erwartet werden Kopfzeilen wie
|
||||||
|
<Box component="code" sx={{ display: 'block', mt: 0.5, fontFamily: 'monospace', fontSize: '0.72rem' }}>
|
||||||
|
{'// @applet-input-schema {"type":"object","properties":{...}}'}
|
||||||
|
<br />
|
||||||
|
{'// @applet-output-schema {"type":"object","properties":{...}}'}
|
||||||
|
</Box>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,9 @@ import vfsAutomat from '../../automats/vfsAutomat';
|
|||||||
import VfsFileExplorer from './VfsFileExplorer';
|
import VfsFileExplorer from './VfsFileExplorer';
|
||||||
import VfsTextEditor from './VfsTextEditor';
|
import VfsTextEditor from './VfsTextEditor';
|
||||||
import VfsFilePreview from './VfsFilePreview';
|
import VfsFilePreview from './VfsFilePreview';
|
||||||
|
import VfsAppletRunner from './VfsAppletRunner';
|
||||||
import VfsNewFileDialog from './VfsNewFileDialog';
|
import VfsNewFileDialog from './VfsNewFileDialog';
|
||||||
|
import { isAppletFile } from './vfsUtils';
|
||||||
|
|
||||||
export default class VfsEditorPanel extends PureComponent {
|
export default class VfsEditorPanel extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -31,6 +33,8 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
searchQuery: s.searchQuery,
|
searchQuery: s.searchQuery,
|
||||||
newFileDialogOpen: s.newFileDialogOpen,
|
newFileDialogOpen: s.newFileDialogOpen,
|
||||||
newFileDefaultFolder: s.newFileDefaultFolder,
|
newFileDefaultFolder: s.newFileDefaultFolder,
|
||||||
|
// Applet runner view: 'run' shows input/output forms, 'code' shows text editor.
|
||||||
|
appletView: 'run',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +62,15 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
// Reset to runner view whenever another file gets selected.
|
||||||
|
if (prevState.selectedFileName !== this.state.selectedFileName) {
|
||||||
|
if (this.state.appletView !== 'run') {
|
||||||
|
this.setState({ appletView: 'run' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unsub?.();
|
this.unsub?.();
|
||||||
}
|
}
|
||||||
@@ -86,6 +99,13 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleRunApplet = (fileName) => {
|
||||||
|
if (!fileName) return;
|
||||||
|
// Switch to runner view first so the forms show immediately on select.
|
||||||
|
this.setState({ appletView: 'run' });
|
||||||
|
void vfsAutomat.actions.selectFile(fileName);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
files,
|
files,
|
||||||
@@ -100,8 +120,12 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
saveError,
|
saveError,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
newFileDialogOpen,
|
newFileDialogOpen,
|
||||||
|
appletView,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const isApplet = Boolean(selectedFileName && isAppletFile(selectedFileName));
|
||||||
|
const showAppletRunner = isApplet && selectedFile?.isText && appletView === 'run';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -115,7 +139,18 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedFileName && selectedFile?.isText ? (
|
{showAppletRunner ? (
|
||||||
|
/* Applet Runner Mode: Input/Output forms + no-op Run button */
|
||||||
|
<VfsAppletRunner
|
||||||
|
fileName={selectedFileName}
|
||||||
|
file={selectedFile}
|
||||||
|
content={fileContent}
|
||||||
|
loadingContent={loadingContent}
|
||||||
|
onClose={() => vfsAutomat.actions.selectFile(null)}
|
||||||
|
onShowCode={() => this.setState({ appletView: 'code' })}
|
||||||
|
onMentionInChat={() => this.handleMentionInChat(selectedFile)}
|
||||||
|
/>
|
||||||
|
) : selectedFileName && selectedFile?.isText ? (
|
||||||
/* Single Column Mode: Text Editor */
|
/* Single Column Mode: Text Editor */
|
||||||
<VfsTextEditor
|
<VfsTextEditor
|
||||||
fileName={selectedFileName}
|
fileName={selectedFileName}
|
||||||
@@ -131,6 +166,9 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
onClose={() => vfsAutomat.actions.selectFile(null)}
|
onClose={() => vfsAutomat.actions.selectFile(null)}
|
||||||
onDownload={() => vfsAutomat.actions.downloadFile(selectedFileName)}
|
onDownload={() => vfsAutomat.actions.downloadFile(selectedFileName)}
|
||||||
onMentionInChat={(lineInfo) => this.handleMentionInChat(selectedFile, lineInfo)}
|
onMentionInChat={(lineInfo) => this.handleMentionInChat(selectedFile, lineInfo)}
|
||||||
|
// Applets get an exe affordance to switch back to the runner forms.
|
||||||
|
isApplet={isApplet}
|
||||||
|
onShowRunner={isApplet ? () => this.setState({ appletView: 'run' }) : undefined}
|
||||||
/>
|
/>
|
||||||
) : selectedFileName && !selectedFile?.isText ? (
|
) : selectedFileName && !selectedFile?.isText ? (
|
||||||
/* Single Column Mode: Preview (Image, PDF, Binary) */
|
/* Single Column Mode: Preview (Image, PDF, Binary) */
|
||||||
@@ -181,6 +219,7 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
selectedFileName={selectedFileName}
|
selectedFileName={selectedFileName}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
onSelectFile={(name) => vfsAutomat.actions.selectFile(name)}
|
onSelectFile={(name) => vfsAutomat.actions.selectFile(name)}
|
||||||
|
onRunApplet={this.handleRunApplet}
|
||||||
onRefresh={() => vfsAutomat.actions.loadFiles()}
|
onRefresh={() => vfsAutomat.actions.loadFiles()}
|
||||||
onOpenNewDialog={(parentDir) => vfsAutomat.actions.setNewFileDialogOpen(true, parentDir)}
|
onOpenNewDialog={(parentDir) => vfsAutomat.actions.setNewFileDialogOpen(true, parentDir)}
|
||||||
onSearchChange={(q) => vfsAutomat.actions.setSearchQuery(q)}
|
onSearchChange={(q) => vfsAutomat.actions.setSearchQuery(q)}
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ import ContentCopyRoundedIcon from '@mui/icons-material/ContentCopyRounded';
|
|||||||
import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
|
import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
|
||||||
import CreateNewFolderRoundedIcon from '@mui/icons-material/CreateNewFolderRounded';
|
import CreateNewFolderRoundedIcon from '@mui/icons-material/CreateNewFolderRounded';
|
||||||
import DriveFileRenameOutlineRoundedIcon from '@mui/icons-material/DriveFileRenameOutlineRounded';
|
import DriveFileRenameOutlineRoundedIcon from '@mui/icons-material/DriveFileRenameOutlineRounded';
|
||||||
|
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
|
||||||
|
|
||||||
import VfsFileIcon from './VfsFileIcon';
|
import VfsFileIcon from './VfsFileIcon';
|
||||||
import { formatVfsSize, formatVfsDate } from './vfsUtils';
|
import { formatVfsSize, formatVfsDate, isAppletFile } from './vfsUtils';
|
||||||
import VfsNewFolderDialog from './VfsNewFolderDialog';
|
import VfsNewFolderDialog from './VfsNewFolderDialog';
|
||||||
import VfsRenameDialog from './VfsRenameDialog';
|
import VfsRenameDialog from './VfsRenameDialog';
|
||||||
|
|
||||||
@@ -415,8 +416,9 @@ export default class VfsFileExplorer extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderFileNode = (file, depth) => {
|
renderFileNode = (file, depth) => {
|
||||||
const { selectedFileName, onSelectFile } = this.props;
|
const { selectedFileName, onSelectFile, onRunApplet } = this.props;
|
||||||
const isSelected = file.path === selectedFileName;
|
const isSelected = file.path === selectedFileName;
|
||||||
|
const isApplet = isAppletFile(file.path);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -486,6 +488,23 @@ export default class VfsFileExplorer extends PureComponent {
|
|||||||
'&:hover': { opacity: 1 },
|
'&:hover': { opacity: 1 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{isApplet && (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onRunApplet) {
|
||||||
|
onRunApplet(file.path);
|
||||||
|
} else {
|
||||||
|
onSelectFile(file.path);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label="Applet ausf\u00fchren"
|
||||||
|
sx={{ p: 0.5, color: 'success.main' }}
|
||||||
|
>
|
||||||
|
<PlayArrowRoundedIcon sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={(e) => this.handleOpenMenu(e, file.fileData)}
|
onClick={(e) => this.handleOpenMenu(e, file.fileData)}
|
||||||
|
|||||||
@@ -8,9 +8,15 @@ import DescriptionRoundedIcon from '@mui/icons-material/DescriptionRounded';
|
|||||||
import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
|
import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
|
||||||
import InsertDriveFileRoundedIcon from '@mui/icons-material/InsertDriveFileRounded';
|
import InsertDriveFileRoundedIcon from '@mui/icons-material/InsertDriveFileRounded';
|
||||||
import ArticleRoundedIcon from '@mui/icons-material/ArticleRounded';
|
import ArticleRoundedIcon from '@mui/icons-material/ArticleRounded';
|
||||||
|
import SmartToyRoundedIcon from '@mui/icons-material/SmartToyRounded';
|
||||||
import { getVfsCategory } from './vfsUtils';
|
import { getVfsCategory } from './vfsUtils';
|
||||||
|
|
||||||
const CATEGORY_CONFIG = {
|
const CATEGORY_CONFIG = {
|
||||||
|
applet: {
|
||||||
|
icon: SmartToyRoundedIcon,
|
||||||
|
color: '#00897b', // Teal
|
||||||
|
label: 'Applet',
|
||||||
|
},
|
||||||
skill: {
|
skill: {
|
||||||
icon: AutoAwesomeRoundedIcon,
|
icon: AutoAwesomeRoundedIcon,
|
||||||
color: '#9c27b0', // Purple
|
color: '#9c27b0', // Purple
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import UndoRoundedIcon from '@mui/icons-material/UndoRounded';
|
|||||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||||
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||||
import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
|
import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
|
||||||
|
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
|
|
||||||
import VfsFileIcon from './VfsFileIcon';
|
import VfsFileIcon from './VfsFileIcon';
|
||||||
@@ -153,6 +154,8 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
onSave,
|
onSave,
|
||||||
onClose,
|
onClose,
|
||||||
onMentionInChat,
|
onMentionInChat,
|
||||||
|
isApplet,
|
||||||
|
onShowRunner,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const lineCount = (content || '').split('\n').length;
|
const lineCount = (content || '').split('\n').length;
|
||||||
@@ -211,6 +214,24 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexShrink: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexShrink: 0 }}>
|
||||||
|
{isApplet && onShowRunner && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
color="success"
|
||||||
|
startIcon={<PlayArrowRoundedIcon />}
|
||||||
|
onClick={onShowRunner}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.4,
|
||||||
|
fontSize: '0.78rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Exe
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{onMentionInChat && (
|
{onMentionInChat && (
|
||||||
<IconButton size="small" onClick={this.handleMentionInChat} aria-label="In Chat erwähnen" sx={{ p: 0.6 }}>
|
<IconButton size="small" onClick={this.handleMentionInChat} aria-label="In Chat erwähnen" sx={{ p: 0.6 }}>
|
||||||
<ChatBubbleOutlineRoundedIcon fontSize="small" color="primary" />
|
<ChatBubbleOutlineRoundedIcon fontSize="small" color="primary" />
|
||||||
|
|||||||
@@ -3,4 +3,6 @@ export { default as VfsFileExplorer } from './VfsFileExplorer';
|
|||||||
export { default as VfsTextEditor } from './VfsTextEditor';
|
export { default as VfsTextEditor } from './VfsTextEditor';
|
||||||
export { default as VfsFilePreview } from './VfsFilePreview';
|
export { default as VfsFilePreview } from './VfsFilePreview';
|
||||||
export { default as VfsFileIcon } from './VfsFileIcon';
|
export { default as VfsFileIcon } from './VfsFileIcon';
|
||||||
|
export { default as VfsAppletForm } from './VfsAppletForm';
|
||||||
|
export { default as VfsAppletRunner } from './VfsAppletRunner';
|
||||||
export * from './vfsUtils';
|
export * from './vfsUtils';
|
||||||
|
|||||||
@@ -41,12 +41,13 @@ export function formatVfsDate(isoString) {
|
|||||||
* Detect category of file.
|
* Detect category of file.
|
||||||
* @param {string} filePath
|
* @param {string} filePath
|
||||||
* @param {string} [mime]
|
* @param {string} [mime]
|
||||||
* @returns {'code'|'table'|'markdown'|'text'|'pdf'|'image'|'skill'|'binary'}
|
* @returns {'code'|'table'|'markdown'|'text'|'pdf'|'image'|'skill'|'applet'|'binary'}
|
||||||
*/
|
*/
|
||||||
export function getVfsCategory(filePath, mime = '') {
|
export function getVfsCategory(filePath, mime = '') {
|
||||||
const p = String(filePath || '').toLowerCase();
|
const p = String(filePath || '').toLowerCase();
|
||||||
const m = String(mime || '').toLowerCase();
|
const m = String(mime || '').toLowerCase();
|
||||||
|
|
||||||
|
if (isAppletFile(p)) return 'applet';
|
||||||
if (p.endsWith('.skill')) return 'skill';
|
if (p.endsWith('.skill')) return 'skill';
|
||||||
if (p.endsWith('.pdf') || m === 'application/pdf') return 'pdf';
|
if (p.endsWith('.pdf') || m === 'application/pdf') return 'pdf';
|
||||||
if (
|
if (
|
||||||
@@ -79,6 +80,17 @@ export function getVfsCategory(filePath, mime = '') {
|
|||||||
return 'binary';
|
return 'binary';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a VFS path is an executable applet file.
|
||||||
|
* Applets are `*.apple.js` (also accepts `*.applet.js`).
|
||||||
|
* @param {string} filePath
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isAppletFile(filePath) {
|
||||||
|
const p = String(filePath || '').toLowerCase();
|
||||||
|
return p.endsWith('.apple.js') || p.endsWith('.applet.js');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split a VFS file path into directory and basename.
|
* Split a VFS file path into directory and basename.
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
@@ -95,3 +107,77 @@ export function splitVfsPath(path) {
|
|||||||
base: clean.slice(lastSlash + 1),
|
base: clean.slice(lastSlash + 1),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a JSON schema object from a `// @applet-*-schema {...}` marker line.
|
||||||
|
* @param {string} content
|
||||||
|
* @param {'input'|'output'} kind
|
||||||
|
* @returns {object|null}
|
||||||
|
*/
|
||||||
|
function extractAppletSchema(content, kind) {
|
||||||
|
const marker = kind === 'input' ? '@applet-input-schema' : '@applet-output-schema';
|
||||||
|
const lines = String(content || '').split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const idx = line.indexOf(marker);
|
||||||
|
if (idx === -1) continue;
|
||||||
|
const jsonPart = line.slice(idx + marker.length).trim();
|
||||||
|
if (!jsonPart) continue;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(jsonPart);
|
||||||
|
if (parsed && typeof parsed === 'object') return parsed;
|
||||||
|
} catch {
|
||||||
|
// ignore malformed schema lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse applet input/output JSON schemas from file content.
|
||||||
|
* Looks for `// @applet-input-schema {...}` and `// @applet-output-schema {...}`.
|
||||||
|
* @param {string} content
|
||||||
|
* @returns {{ inputSchema: object|null, outputSchema: object|null }}
|
||||||
|
*/
|
||||||
|
export function parseAppletSchemas(content) {
|
||||||
|
return {
|
||||||
|
inputSchema: extractAppletSchema(content, 'input'),
|
||||||
|
outputSchema: extractAppletSchema(content, 'output'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a default values object from a JSON schema (`properties` + `default`).
|
||||||
|
* @param {object|null} schema
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function buildAppletDefaults(schema) {
|
||||||
|
const values = {};
|
||||||
|
const props = schema?.properties;
|
||||||
|
if (!props || typeof props !== 'object') return values;
|
||||||
|
for (const key of Object.keys(props)) {
|
||||||
|
const prop = props[key] || {};
|
||||||
|
if (prop.default !== undefined) {
|
||||||
|
values[key] = prop.default;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (prop.type) {
|
||||||
|
case 'boolean':
|
||||||
|
values[key] = false;
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
case 'integer':
|
||||||
|
values[key] = '';
|
||||||
|
break;
|
||||||
|
case 'array':
|
||||||
|
values[key] = [];
|
||||||
|
break;
|
||||||
|
case 'object':
|
||||||
|
values[key] = {};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
values[key] = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user