feat: phase 1 of python scripting
This commit is contained in:
@@ -30,6 +30,12 @@ const MediaIcon = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ScriptsIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20 3H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7v2H8v2h8v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 14V5h14v9H5zm2-7.5L9.5 9 7 11.5l1.4 1.4L12.3 9 8.4 5.1 7 6.5zm6.5 5.5h4v-2h-4v2z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const SettingsIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>
|
||||
@@ -170,6 +176,13 @@ export const ActivityBar: React.FC = () => {
|
||||
>
|
||||
<MediaIcon />
|
||||
</button>
|
||||
<button
|
||||
className={`activity-bar-item ${isActivityActive(snapshot, 'scripts') ? 'active' : ''}`}
|
||||
onClick={() => executeActivityClick('scripts')}
|
||||
title={getTitle('scripts')}
|
||||
>
|
||||
<ScriptsIcon />
|
||||
</button>
|
||||
<button
|
||||
className={`activity-bar-item ${isActivityActive(snapshot, 'tags') ? 'active' : ''}`}
|
||||
onClick={() => executeActivityClick('tags')}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { MetadataDiffPanel } from '../MetadataDiffPanel';
|
||||
import { GitDiffView } from '../GitDiffView/GitDiffView';
|
||||
import { DocumentationView } from '../DocumentationView/DocumentationView';
|
||||
import { SiteValidationView } from '../SiteValidationView';
|
||||
import { ScriptsView } from '../ScriptsView/ScriptsView';
|
||||
import { AutoSaveManager, getContrastColor } from '../../utils';
|
||||
import { InsertModal } from '../InsertModal';
|
||||
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
|
||||
@@ -1796,6 +1797,7 @@ export const Editor: React.FC = () => {
|
||||
: <Dashboard />,
|
||||
documentation: () => <DocumentationView />,
|
||||
'site-validation': () => <SiteValidationView />,
|
||||
scripts: () => <ScriptsView scriptId={editorRoute.tabId} />,
|
||||
post: () => (editorRoute.tabId ? <PostEditor key={editorRoute.tabId} postId={editorRoute.tabId} /> : <Dashboard />),
|
||||
media: () => (editorRoute.tabId ? <MediaEditor key={editorRoute.tabId} mediaId={editorRoute.tabId} /> : <Dashboard />),
|
||||
dashboard: () => <Dashboard />,
|
||||
|
||||
@@ -86,6 +86,22 @@
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.output-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.output-item {
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-editor-foreground);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.task-group-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -43,6 +43,7 @@ export const Panel: React.FC = () => {
|
||||
panelVisible,
|
||||
panelActiveTab,
|
||||
setPanelActiveTab,
|
||||
panelOutputEntries,
|
||||
tasks,
|
||||
tabs,
|
||||
activeTabId,
|
||||
@@ -383,7 +384,17 @@ export const Panel: React.FC = () => {
|
||||
)}
|
||||
|
||||
{effectiveActivePanelTab === 'output' && (
|
||||
<div className="panel-empty">{t('panel.noOutput')}</div>
|
||||
panelOutputEntries.length === 0 ? (
|
||||
<div className="panel-empty">{t('panel.noOutput')}</div>
|
||||
) : (
|
||||
<div className="output-list">
|
||||
{panelOutputEntries.map((entry) => (
|
||||
<div key={entry.id} className={`output-item output-${entry.kind}`}>
|
||||
{entry.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{effectiveActivePanelTab === 'post-links' && (
|
||||
|
||||
32
src/renderer/components/ScriptsView/ScriptsView.css
Normal file
32
src/renderer/components/ScriptsView/ScriptsView.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.scripts-view {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scripts-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.scripts-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.scripts-label {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.scripts-textarea {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
114
src/renderer/components/ScriptsView/ScriptsView.tsx
Normal file
114
src/renderer/components/ScriptsView/ScriptsView.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ScriptData } from '../../../main/shared/electronApi';
|
||||
import { useAppStore } from '../../store';
|
||||
import { getPythonRuntimeManager } from '../../python/runtimeManagerInstance';
|
||||
import { useI18n } from '../../i18n';
|
||||
import './ScriptsView.css';
|
||||
|
||||
interface ScriptsViewProps {
|
||||
scriptId: string | null;
|
||||
}
|
||||
|
||||
export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
|
||||
const { t } = useI18n();
|
||||
const appendPanelOutputEntry = useAppStore((state) => state.appendPanelOutputEntry);
|
||||
const [script, setScript] = useState<ScriptData | null>(null);
|
||||
const [scriptContent, setScriptContent] = useState('');
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const loadScript = async () => {
|
||||
if (!scriptId) {
|
||||
setScript(null);
|
||||
setScriptContent('');
|
||||
return;
|
||||
}
|
||||
|
||||
const item = await window.electronAPI?.scripts.get(scriptId);
|
||||
if (cancelled || !item) {
|
||||
setScript(null);
|
||||
setScriptContent('');
|
||||
return;
|
||||
}
|
||||
|
||||
setScript(item);
|
||||
setScriptContent(item.content || '');
|
||||
};
|
||||
|
||||
void loadScript();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [scriptId]);
|
||||
|
||||
const handleRunScript = async () => {
|
||||
if (!script || isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRunning(true);
|
||||
|
||||
try {
|
||||
const runtimeManager = getPythonRuntimeManager();
|
||||
const result = await runtimeManager.execute(scriptContent);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
if (result.result.trim().length > 0) {
|
||||
appendPanelOutputEntry({
|
||||
id: `output-${Date.now()}-result`,
|
||||
message: result.result,
|
||||
createdAt: now,
|
||||
kind: 'result',
|
||||
});
|
||||
}
|
||||
|
||||
if (result.stdout.trim().length > 0) {
|
||||
appendPanelOutputEntry({
|
||||
id: `output-${Date.now()}-stdout`,
|
||||
message: result.stdout,
|
||||
createdAt: now,
|
||||
kind: 'stdout',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
appendPanelOutputEntry({
|
||||
id: `output-${Date.now()}-error`,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
createdAt: new Date().toISOString(),
|
||||
kind: 'error',
|
||||
});
|
||||
} finally {
|
||||
useAppStore.setState({
|
||||
panelVisible: true,
|
||||
panelActiveTab: 'output',
|
||||
});
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="scripts-view">
|
||||
<div className="scripts-editor">
|
||||
<div className="scripts-toolbar">
|
||||
<button type="button" onClick={handleRunScript} disabled={!script || isRunning}>
|
||||
{t('scripts.run')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label className="scripts-label" htmlFor="scripts-content">
|
||||
{t('scripts.content')}
|
||||
</label>
|
||||
<textarea
|
||||
id="scripts-content"
|
||||
className="scripts-textarea"
|
||||
value={scriptContent}
|
||||
onChange={(event) => setScriptContent(event.target.value)}
|
||||
disabled={!script}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import { scrollToSettingsSection, SettingsCategory } from '../SettingsView/Setti
|
||||
import { scrollToTagsSection, TagsCategory } from '../TagsView';
|
||||
import { activateSidebarSection } from '../../navigation/sectionActivation';
|
||||
import { getPersistedSidebarSection, setPersistedSidebarSection } from '../../navigation/sidebarUiPersistence';
|
||||
import { openChatTab, openEntityTab, openImportTab, openSingletonToolTab } from '../../navigation/tabPolicy';
|
||||
import { openChatTab, openEntityTab, openImportTab, openScriptTab, openSingletonToolTab } from '../../navigation/tabPolicy';
|
||||
import { createAndFocusPost } from '../../navigation/postCreation';
|
||||
import type { SidebarView } from '../../navigation/sidebarViewRegistry';
|
||||
import { useI18n } from '../../i18n';
|
||||
@@ -1666,6 +1666,125 @@ const ImportList: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ScriptsList: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const { openTab, activeTabId } = useAppStore();
|
||||
const [scripts, setScripts] = useState<Array<{ id: string; title: string }>>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const loadScripts = useCallback(async () => {
|
||||
const items = await window.electronAPI?.scripts.getAll();
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
setScripts(items.map((item) => ({ id: item.id, title: item.title })));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const loadInitialScripts = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const items = await window.electronAPI?.scripts.getAll();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setScripts((items ?? []).map((item) => ({ id: item.id, title: item.title })));
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void loadInitialScripts();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCreateScript = async () => {
|
||||
try {
|
||||
const created = await window.electronAPI?.scripts.create({
|
||||
title: t('sidebar.scripts.newScript'),
|
||||
kind: 'utility',
|
||||
content: 'print("new script")',
|
||||
entrypoint: 'render',
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
if (!created) {
|
||||
return;
|
||||
}
|
||||
|
||||
setScripts((prev) => [
|
||||
{ id: created.id, title: created.title },
|
||||
...prev.filter((script) => script.id !== created.id),
|
||||
]);
|
||||
openScriptTab(openTab, created.id, 'pin');
|
||||
void loadScripts();
|
||||
} catch (error) {
|
||||
console.error('Failed to create script:', error);
|
||||
showToast.error(t('sidebar.scripts.createFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="chat-list">
|
||||
<div className="chat-list-header">
|
||||
<span>{t('sidebar.scripts.header')}</span>
|
||||
</div>
|
||||
<div className="chat-loading">{t('sidebar.loading')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-list">
|
||||
<div className="chat-list-header">
|
||||
<span>{t('sidebar.scripts.header')}</span>
|
||||
<button
|
||||
className="chat-new-button"
|
||||
onClick={handleCreateScript}
|
||||
aria-label={t('sidebar.scripts.newScript')}
|
||||
title={t('sidebar.scripts.newScript')}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="chat-list-items">
|
||||
{scripts.length === 0 ? (
|
||||
<div className="chat-empty">
|
||||
<p>{t('sidebar.scripts.none')}</p>
|
||||
<button className="chat-start-button" onClick={handleCreateScript}>
|
||||
{t('sidebar.scripts.createScript')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
scripts.map((script) => (
|
||||
<button
|
||||
key={script.id}
|
||||
type="button"
|
||||
className={`chat-list-item ${activeTabId === script.id ? 'active' : ''}`}
|
||||
onClick={() => openScriptTab(openTab, script.id, 'preview')}
|
||||
onDoubleClick={() => openScriptTab(openTab, script.id, 'pin')}
|
||||
>
|
||||
<div className="chat-item-content">
|
||||
<div className="chat-item-title">{script.title}</div>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Sidebar: React.FC = () => {
|
||||
const { activeView, sidebarVisible } = useAppStore();
|
||||
|
||||
@@ -1677,6 +1796,7 @@ export const Sidebar: React.FC = () => {
|
||||
posts: <PostsList mode="posts" isActive={true} />,
|
||||
pages: <PostsList mode="pages" isActive={true} />,
|
||||
media: <MediaList />,
|
||||
scripts: <ScriptsList />,
|
||||
settings: <SettingsNav />,
|
||||
tags: <TagsNav />,
|
||||
chat: <ChatList />,
|
||||
|
||||
@@ -80,6 +80,10 @@ const getTabTitle = (
|
||||
return tr('siteValidation.tabTitle');
|
||||
}
|
||||
|
||||
if (tab.type === 'scripts') {
|
||||
return tr('tabBar.scripts');
|
||||
}
|
||||
|
||||
return tr('tabBar.unknown');
|
||||
};
|
||||
|
||||
@@ -158,6 +162,12 @@ const getTabIcon = (tab: Tab): React.ReactNode => {
|
||||
<path d="M8 1.5a6.5 6.5 0 1 0 6.5 6.5A6.5 6.5 0 0 0 8 1.5zm0 1a5.5 5.5 0 0 1 4.39 8.82l-.88-.88a.5.5 0 0 0-.7.7l.8.8A5.5 5.5 0 1 1 8 2.5zm2.35 3.15L7 9 5.65 7.65a.5.5 0 1 0-.7.7l1.7 1.7a.5.5 0 0 0 .7 0l3.7-3.7a.5.5 0 1 0-.7-.7z"/>
|
||||
</svg>
|
||||
);
|
||||
case 'scripts':
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20 3H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7v2H8v2h8v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 14V5h14v9H5zm2-7.5L9.5 9 7 11.5l1.4 1.4L12.3 9 8.4 5.1 7 6.5zm6.5 5.5h4v-2h-4v2z"/>
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
|
||||
@@ -26,3 +26,4 @@ export { InsertModal } from './InsertModal';
|
||||
export { WindowTitleBar } from './WindowTitleBar';
|
||||
export { DocumentationView } from './DocumentationView/DocumentationView';
|
||||
export { SiteValidationView } from './SiteValidationView';
|
||||
export { ScriptsView } from './ScriptsView/ScriptsView';
|
||||
|
||||
Reference in New Issue
Block a user