Feat/language detection (#31)
* feat: implementation of language detection * run utility scripts in tasks * fix: addiitonal fixes for background utilities * feat: toast() also for utility scripts --------- Co-authored-by: hugo <hugoms@me.com>
This commit is contained in:
@@ -192,6 +192,23 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.editor-language-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editor-language-row select {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.editor-language-row button.compact {
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
min-width: unset;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.editor-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -75,6 +75,9 @@ const autoSaveManager = new AutoSaveManager({
|
||||
if ('templateSlug' in changes) {
|
||||
(update as Record<string, unknown>).templateSlug = changes.templateSlug as string || null;
|
||||
}
|
||||
if ('language' in changes) {
|
||||
update.language = changes.language as string || undefined;
|
||||
}
|
||||
|
||||
const updated = await window.electronAPI?.posts.update(id, update);
|
||||
if (updated) {
|
||||
@@ -196,8 +199,10 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>(['article']);
|
||||
const [templateSlug, setTemplateSlug] = useState('');
|
||||
const [postLanguage, setPostLanguage] = useState('');
|
||||
const [availablePostTemplates, setAvailablePostTemplates] = useState<Array<{ slug: string; title: string }>>([]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDetectingLanguage, setIsDetectingLanguage] = useState(false);
|
||||
const [hasPublishedVersion, setHasPublishedVersion] = useState(false);
|
||||
const [editorMode, setEditorMode] = useState<EditorMode>(preferredEditorMode);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
@@ -326,6 +331,7 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
setTags(post.tags);
|
||||
setSelectedCategories(post.categories.length > 0 ? post.categories : ['article']);
|
||||
setTemplateSlug((post as PostData & { templateSlug?: string }).templateSlug || '');
|
||||
setPostLanguage(post.language || '');
|
||||
setMetadataExpanded(post.title === '');
|
||||
markClean(postId);
|
||||
// Mark as initialized AFTER setting local state
|
||||
@@ -347,7 +353,8 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
const titleChanged = title !== post.title;
|
||||
const authorChanged = author !== (post.author || '');
|
||||
const templateSlugChanged = templateSlug !== ((post as PostData & { templateSlug?: string }).templateSlug || '');
|
||||
const hasChanges = contentChanged || titleChanged || authorChanged || templateSlugChanged ||
|
||||
const languageChanged = postLanguage !== (post.language || '');
|
||||
const hasChanges = contentChanged || titleChanged || authorChanged || templateSlugChanged || languageChanged ||
|
||||
JSON.stringify(tags.slice().sort()) !== JSON.stringify(post.tags.slice().sort()) ||
|
||||
JSON.stringify(selectedCategories.slice().sort()) !== JSON.stringify(post.categories.slice().sort());
|
||||
|
||||
@@ -362,11 +369,12 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
tags: tags.join(', '),
|
||||
categories: selectedCategories,
|
||||
templateSlug: templateSlug || undefined,
|
||||
language: postLanguage || undefined,
|
||||
});
|
||||
} else {
|
||||
markClean(postId);
|
||||
}
|
||||
}, [title, content, author, tags, selectedCategories, templateSlug, post, postId, isInitialized, isDirty, markDirty, markClean]);
|
||||
}, [title, content, author, tags, selectedCategories, templateSlug, postLanguage, post, postId, isInitialized, isDirty, markDirty, markClean]);
|
||||
|
||||
// Handle editor mode change and persist preference
|
||||
const handleEditorModeChange = (mode: EditorMode) => {
|
||||
@@ -386,6 +394,7 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
title,
|
||||
content,
|
||||
author: author || undefined,
|
||||
language: postLanguage || undefined,
|
||||
tags,
|
||||
categories: selectedCategories.length > 0 ? selectedCategories : ['article'],
|
||||
templateSlug: templateSlug || null,
|
||||
@@ -409,6 +418,24 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
}
|
||||
}, [postId, title, content, author, tags, selectedCategories, isDirty, isSaving, updatePost, markClean, showErrorModal]);
|
||||
|
||||
const handleDetectLanguage = useCallback(async () => {
|
||||
if (isDetectingLanguage || (!title && !content)) return;
|
||||
setIsDetectingLanguage(true);
|
||||
try {
|
||||
const result = await window.electronAPI?.chat.detectPostLanguage(title, content);
|
||||
if (result?.success && result.language) {
|
||||
setPostLanguage(result.language);
|
||||
showToast.success(tr('editor.post.quickActions.languageDetected'));
|
||||
} else {
|
||||
showToast.error(result?.error || tr('editor.post.quickActions.detectLanguageFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to detect post language:', error);
|
||||
showToast.error(tr('editor.post.quickActions.detectLanguageFailed'));
|
||||
} finally {
|
||||
setIsDetectingLanguage(false);
|
||||
}
|
||||
}, [title, content, isDetectingLanguage, tr]);
|
||||
const handlePublish = async () => {
|
||||
await handleSave();
|
||||
try {
|
||||
@@ -791,6 +818,30 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
|
||||
placeholder={tr('editor.placeholder.author')}
|
||||
/>
|
||||
</div>
|
||||
<div className="editor-field">
|
||||
<label>{tr('editor.field.language')}</label>
|
||||
<div className="editor-language-row">
|
||||
<select
|
||||
value={postLanguage}
|
||||
onChange={(e) => setPostLanguage(e.target.value)}
|
||||
>
|
||||
<option value="">{tr('editor.field.languageDefault')}</option>
|
||||
<option value="en">{tr('language.en')}</option>
|
||||
<option value="de">{tr('language.de')}</option>
|
||||
<option value="fr">{tr('language.fr')}</option>
|
||||
<option value="it">{tr('language.it')}</option>
|
||||
<option value="es">{tr('language.es')}</option>
|
||||
</select>
|
||||
<button
|
||||
className="secondary compact"
|
||||
onClick={handleDetectLanguage}
|
||||
disabled={isDetectingLanguage || (!title && !content)}
|
||||
title={tr('editor.post.quickActions.detectLanguageDescription')}
|
||||
>
|
||||
{isDetectingLanguage ? tr('editor.post.quickActions.detecting') : '🤖'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="editor-field-row">
|
||||
<div className="editor-field">
|
||||
<label>{tr('editor.field.slug')}</label>
|
||||
|
||||
@@ -363,11 +363,27 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
|
||||
|
||||
setIsRunning(true);
|
||||
|
||||
const isUtility = kind === 'utility';
|
||||
const taskId = isUtility ? `script-${script.id}-${Date.now()}` : undefined;
|
||||
|
||||
if (isUtility && taskId) {
|
||||
await window.electronAPI?.scripts.startTask(taskId, title || script.title);
|
||||
}
|
||||
|
||||
try {
|
||||
const runtimeManager = getPythonRuntimeManager();
|
||||
const result = await runtimeManager.execute(scriptContent, {
|
||||
cacheKey: buildCacheKey(script, scriptContent),
|
||||
entrypoint,
|
||||
timeoutMs: 0,
|
||||
onStdout: (chunk: string) => {
|
||||
appendPanelOutputEntry({
|
||||
id: `output-${Date.now()}-stdout-stream`,
|
||||
message: chunk,
|
||||
createdAt: new Date().toISOString(),
|
||||
kind: 'stdout',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const now = new Date().toISOString();
|
||||
@@ -380,21 +396,21 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (result.stdout.trim().length > 0) {
|
||||
appendPanelOutputEntry({
|
||||
id: `output-${Date.now()}-stdout`,
|
||||
message: result.stdout,
|
||||
createdAt: now,
|
||||
kind: 'stdout',
|
||||
});
|
||||
if (isUtility && taskId) {
|
||||
await window.electronAPI?.scripts.completeTask(taskId);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
appendPanelOutputEntry({
|
||||
id: `output-${Date.now()}-error`,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
message: errorMessage,
|
||||
createdAt: new Date().toISOString(),
|
||||
kind: 'error',
|
||||
});
|
||||
|
||||
if (isUtility && taskId) {
|
||||
await window.electronAPI?.scripts.failTask(taskId, errorMessage);
|
||||
}
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
|
||||
@@ -517,6 +517,13 @@
|
||||
"editor.field.content": "Inhalt",
|
||||
"editor.field.template": "Vorlage",
|
||||
"editor.field.templateDefault": "Standard",
|
||||
"editor.field.language": "Sprache",
|
||||
"editor.field.languageDefault": "Projektstandard",
|
||||
"language.en": "Englisch",
|
||||
"language.de": "Deutsch",
|
||||
"language.fr": "Französisch",
|
||||
"language.it": "Italienisch",
|
||||
"language.es": "Spanisch",
|
||||
"editor.placeholder.tags": "Tags hinzufügen...",
|
||||
"editor.placeholder.author": "Autorenname",
|
||||
"editor.placeholder.categories": "Kategorien hinzufügen...",
|
||||
@@ -879,6 +886,10 @@
|
||||
"editor.media.quickActions.button": "⚡ Schnellaktionen",
|
||||
"editor.media.quickActions.aiTitle": "KI: Titel, Alt-Text und Bildunterschrift erzeugen",
|
||||
"editor.media.quickActions.aiDescription": "Analysiert das Bild und schlägt Metadaten vor",
|
||||
"editor.post.quickActions.detectLanguageDescription": "Sprache mit KI erkennen",
|
||||
"editor.post.quickActions.detecting": "Erkennung…",
|
||||
"editor.post.quickActions.languageDetected": "Sprache erkannt",
|
||||
"editor.post.quickActions.detectLanguageFailed": "Spracherkennung fehlgeschlagen",
|
||||
"editor.media.replaceFile": "Datei ersetzen",
|
||||
"editor.media.field.fileName": "Dateiname",
|
||||
"editor.media.field.type": "Typ",
|
||||
|
||||
@@ -517,6 +517,13 @@
|
||||
"editor.field.content": "Content",
|
||||
"editor.field.template": "Template",
|
||||
"editor.field.templateDefault": "Default",
|
||||
"editor.field.language": "Language",
|
||||
"editor.field.languageDefault": "Project default",
|
||||
"language.en": "English",
|
||||
"language.de": "German",
|
||||
"language.fr": "French",
|
||||
"language.it": "Italian",
|
||||
"language.es": "Spanish",
|
||||
"editor.placeholder.tags": "Add tags...",
|
||||
"editor.placeholder.author": "Author name",
|
||||
"editor.placeholder.categories": "Add categories...",
|
||||
@@ -879,6 +886,10 @@
|
||||
"editor.media.quickActions.button": "⚡ Quick Actions",
|
||||
"editor.media.quickActions.aiTitle": "AI: Generate Title, Alt & Caption",
|
||||
"editor.media.quickActions.aiDescription": "Analyzes the image to suggest metadata",
|
||||
"editor.post.quickActions.detectLanguageDescription": "Detect language using AI",
|
||||
"editor.post.quickActions.detecting": "Detecting…",
|
||||
"editor.post.quickActions.languageDetected": "Language detected",
|
||||
"editor.post.quickActions.detectLanguageFailed": "Language detection failed",
|
||||
"editor.media.replaceFile": "Replace File",
|
||||
"editor.media.field.fileName": "File Name",
|
||||
"editor.media.field.type": "Type",
|
||||
|
||||
@@ -517,6 +517,13 @@
|
||||
"editor.field.content": "Contenido",
|
||||
"editor.field.template": "Plantilla",
|
||||
"editor.field.templateDefault": "Predeterminada",
|
||||
"editor.field.language": "Idioma",
|
||||
"editor.field.languageDefault": "Predeterminado del proyecto",
|
||||
"language.en": "Inglés",
|
||||
"language.de": "Alemán",
|
||||
"language.fr": "Francés",
|
||||
"language.it": "Italiano",
|
||||
"language.es": "Español",
|
||||
"editor.placeholder.tags": "Agregar etiquetas...",
|
||||
"editor.placeholder.author": "Nombre del autor",
|
||||
"editor.placeholder.categories": "Agregar categorías...",
|
||||
@@ -879,6 +886,10 @@
|
||||
"editor.media.quickActions.button": "✨ Analizar con IA",
|
||||
"editor.media.quickActions.aiTitle": "Título sugerido por IA",
|
||||
"editor.media.quickActions.aiDescription": "Genera automáticamente título, texto alternativo y pie de foto.",
|
||||
"editor.post.quickActions.detectLanguageDescription": "Detectar idioma con IA",
|
||||
"editor.post.quickActions.detecting": "Detectando…",
|
||||
"editor.post.quickActions.languageDetected": "Idioma detectado",
|
||||
"editor.post.quickActions.detectLanguageFailed": "Error al detectar el idioma",
|
||||
"editor.media.replaceFile": "Reemplazar archivo",
|
||||
"editor.media.field.fileName": "Nombre de archivo",
|
||||
"editor.media.field.type": "Tipo",
|
||||
|
||||
@@ -517,6 +517,13 @@
|
||||
"editor.field.content": "Contenu",
|
||||
"editor.field.template": "Modèle",
|
||||
"editor.field.templateDefault": "Par défaut",
|
||||
"editor.field.language": "Langue",
|
||||
"editor.field.languageDefault": "Par défaut du projet",
|
||||
"language.en": "Anglais",
|
||||
"language.de": "Allemand",
|
||||
"language.fr": "Français",
|
||||
"language.it": "Italien",
|
||||
"language.es": "Espagnol",
|
||||
"editor.placeholder.tags": "Ajouter des étiquettes...",
|
||||
"editor.placeholder.author": "Nom de l’auteur",
|
||||
"editor.placeholder.categories": "Ajouter des catégories...",
|
||||
@@ -879,6 +886,10 @@
|
||||
"editor.media.quickActions.button": "✨ Analyser avec l’IA",
|
||||
"editor.media.quickActions.aiTitle": "Titre suggéré par l’IA",
|
||||
"editor.media.quickActions.aiDescription": "Générez automatiquement un titre, un texte alternatif et une légende.",
|
||||
"editor.post.quickActions.detectLanguageDescription": "Détecter la langue avec l'IA",
|
||||
"editor.post.quickActions.detecting": "Détection…",
|
||||
"editor.post.quickActions.languageDetected": "Langue détectée",
|
||||
"editor.post.quickActions.detectLanguageFailed": "Échec de la détection de la langue",
|
||||
"editor.media.replaceFile": "Remplacer le fichier",
|
||||
"editor.media.field.fileName": "Nom du fichier",
|
||||
"editor.media.field.type": "Type",
|
||||
|
||||
@@ -517,6 +517,13 @@
|
||||
"editor.field.content": "Contenuto",
|
||||
"editor.field.template": "Modello",
|
||||
"editor.field.templateDefault": "Predefinito",
|
||||
"editor.field.language": "Lingua",
|
||||
"editor.field.languageDefault": "Predefinito del progetto",
|
||||
"language.en": "Inglese",
|
||||
"language.de": "Tedesco",
|
||||
"language.fr": "Francese",
|
||||
"language.it": "Italiano",
|
||||
"language.es": "Spagnolo",
|
||||
"editor.placeholder.tags": "Aggiungi tag...",
|
||||
"editor.placeholder.author": "Nome autore",
|
||||
"editor.placeholder.categories": "Aggiungi categorie...",
|
||||
@@ -879,6 +886,10 @@
|
||||
"editor.media.quickActions.button": "✨ Analizza con IA",
|
||||
"editor.media.quickActions.aiTitle": "Titolo suggerito dall’IA",
|
||||
"editor.media.quickActions.aiDescription": "Genera automaticamente titolo, testo alternativo e didascalia.",
|
||||
"editor.post.quickActions.detectLanguageDescription": "Rileva la lingua con l'IA",
|
||||
"editor.post.quickActions.detecting": "Rilevamento…",
|
||||
"editor.post.quickActions.languageDetected": "Lingua rilevata",
|
||||
"editor.post.quickActions.detectLanguageFailed": "Rilevamento lingua non riuscito",
|
||||
"editor.media.replaceFile": "Sostituisci file",
|
||||
"editor.media.field.fileName": "Nome file",
|
||||
"editor.media.field.type": "Tipo",
|
||||
|
||||
@@ -3,12 +3,22 @@ import type { PythonWorkerMessage, PythonWorkerRequest } from './runtimeProtocol
|
||||
import type { PythonSyntaxError } from './runtimeProtocol';
|
||||
import { parseMacroContextV1, parseMacroResultV1, type MacroContextV1, type MacroResultV1 } from './abiV1';
|
||||
import { invokePythonApiMethodV1 } from './pythonApiInvokerV1';
|
||||
import { showToast } from '../components/Toast';
|
||||
|
||||
type WorkerFactory = () => Worker;
|
||||
type PythonApiInvoker = (method: string, args: unknown) => Promise<unknown>;
|
||||
type ToastHandler = (message: string, toastType?: string) => void;
|
||||
|
||||
const TOAST_TYPES = new Set(['success', 'error', 'info']);
|
||||
|
||||
function defaultToastHandler(message: string, toastType?: string): void {
|
||||
const resolvedType = (toastType && TOAST_TYPES.has(toastType) ? toastType : 'info') as 'success' | 'error' | 'info';
|
||||
showToast[resolvedType](message);
|
||||
}
|
||||
|
||||
interface PythonRuntimeManagerOptions {
|
||||
invokeApiCall?: PythonApiInvoker;
|
||||
onToast?: ToastHandler;
|
||||
}
|
||||
|
||||
interface InitializeDeferred {
|
||||
@@ -22,6 +32,8 @@ interface PendingRun {
|
||||
resolve: (value: PythonRunResult | PythonMacroV1Result | string[] | PythonSyntaxCheckResult) => void;
|
||||
reject: (error: Error) => void;
|
||||
timeoutId: ReturnType<typeof setTimeout> | null;
|
||||
timeoutMs: number;
|
||||
onStdout?: (chunk: string) => void;
|
||||
}
|
||||
|
||||
export interface PythonRunResult {
|
||||
@@ -33,6 +45,7 @@ export interface PythonExecuteOptions {
|
||||
timeoutMs?: number;
|
||||
cacheKey?: string;
|
||||
entrypoint?: string;
|
||||
onStdout?: (chunk: string) => void;
|
||||
}
|
||||
|
||||
export interface PythonMacroSourceOptions {
|
||||
@@ -65,12 +78,14 @@ export class PythonRuntimeManager {
|
||||
private activeRequestId: string | null = null;
|
||||
private requestCounter = 0;
|
||||
private readonly invokeApiCall: PythonApiInvoker;
|
||||
private readonly onToast: ToastHandler;
|
||||
|
||||
constructor(
|
||||
private readonly workerFactory: WorkerFactory = createPythonRuntimeWorker,
|
||||
options: PythonRuntimeManagerOptions = {}
|
||||
) {
|
||||
this.invokeApiCall = options.invokeApiCall ?? invokePythonApiMethodV1;
|
||||
this.onToast = options.onToast ?? defaultToastHandler;
|
||||
}
|
||||
|
||||
initialize(): Promise<void> {
|
||||
@@ -116,18 +131,14 @@ export class PythonRuntimeManager {
|
||||
const timeoutMs = options?.timeoutMs ?? 5000;
|
||||
|
||||
return new Promise<PythonRunResult>((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.pendingRuns.delete(requestId);
|
||||
this.resetRuntime(`Python script execution timed out after ${timeoutMs}ms`);
|
||||
reject(new Error(`Python script execution timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
this.pendingRuns.set(requestId, {
|
||||
kind: 'run',
|
||||
stdout: '',
|
||||
resolve: (value) => resolve(value as PythonRunResult),
|
||||
reject,
|
||||
timeoutId,
|
||||
timeoutId: null,
|
||||
timeoutMs,
|
||||
onStdout: options?.onStdout,
|
||||
});
|
||||
|
||||
const message: PythonWorkerRequest = {
|
||||
@@ -155,18 +166,13 @@ export class PythonRuntimeManager {
|
||||
const timeoutMs = options?.timeoutMs ?? 5000;
|
||||
|
||||
return new Promise<PythonMacroV1Result>((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.pendingRuns.delete(requestId);
|
||||
this.resetRuntime(`Python script execution timed out after ${timeoutMs}ms`);
|
||||
reject(new Error(`Python script execution timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
this.pendingRuns.set(requestId, {
|
||||
kind: 'macro-v1',
|
||||
stdout: '',
|
||||
resolve: (value) => resolve(value as PythonMacroV1Result),
|
||||
reject,
|
||||
timeoutId,
|
||||
timeoutId: null,
|
||||
timeoutMs,
|
||||
});
|
||||
|
||||
const message: PythonWorkerRequest = {
|
||||
@@ -194,18 +200,13 @@ export class PythonRuntimeManager {
|
||||
const timeoutMs = options?.timeoutMs ?? 5000;
|
||||
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.pendingRuns.delete(requestId);
|
||||
this.resetRuntime(`Python script execution timed out after ${timeoutMs}ms`);
|
||||
reject(new Error(`Python script execution timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
this.pendingRuns.set(requestId, {
|
||||
kind: 'inspect-entrypoints',
|
||||
stdout: '',
|
||||
resolve: (value) => resolve(value as string[]),
|
||||
reject,
|
||||
timeoutId,
|
||||
timeoutId: null,
|
||||
timeoutMs,
|
||||
});
|
||||
|
||||
const message: PythonWorkerRequest = {
|
||||
@@ -230,18 +231,13 @@ export class PythonRuntimeManager {
|
||||
const timeoutMs = options?.timeoutMs ?? 5000;
|
||||
|
||||
return new Promise<PythonSyntaxCheckResult>((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.pendingRuns.delete(requestId);
|
||||
this.resetRuntime(`Python script execution timed out after ${timeoutMs}ms`);
|
||||
reject(new Error(`Python script execution timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
this.pendingRuns.set(requestId, {
|
||||
kind: 'syntax-check',
|
||||
stdout: '',
|
||||
resolve: (value) => resolve(value as PythonSyntaxCheckResult),
|
||||
reject,
|
||||
timeoutId,
|
||||
timeoutId: null,
|
||||
timeoutMs,
|
||||
});
|
||||
|
||||
const message: PythonWorkerRequest = {
|
||||
@@ -282,6 +278,11 @@ export class PythonRuntimeManager {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'toast') {
|
||||
this.onToast(payload.message, payload.toastType);
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingRun = this.pendingRuns.get(payload.requestId);
|
||||
if (!pendingRun) {
|
||||
if (this.activeRequestId === payload.requestId && payload.type !== 'stdout') {
|
||||
@@ -293,6 +294,7 @@ export class PythonRuntimeManager {
|
||||
|
||||
if (payload.type === 'stdout') {
|
||||
pendingRun.stdout += payload.chunk;
|
||||
pendingRun.onStdout?.(payload.chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -440,6 +442,7 @@ export class PythonRuntimeManager {
|
||||
}
|
||||
|
||||
this.activeRequestId = request.requestId;
|
||||
this.startTimeoutForRequest(request.requestId);
|
||||
this.worker.postMessage(request);
|
||||
}
|
||||
|
||||
@@ -454,9 +457,23 @@ export class PythonRuntimeManager {
|
||||
}
|
||||
|
||||
this.activeRequestId = nextRequest.requestId;
|
||||
this.startTimeoutForRequest(nextRequest.requestId);
|
||||
this.worker.postMessage(nextRequest);
|
||||
}
|
||||
|
||||
private startTimeoutForRequest(requestId: string): void {
|
||||
const pendingRun = this.pendingRuns.get(requestId);
|
||||
if (!pendingRun || pendingRun.timeoutMs <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingRun.timeoutId = setTimeout(() => {
|
||||
this.pendingRuns.delete(requestId);
|
||||
this.resetRuntime(`Python script execution timed out after ${pendingRun.timeoutMs}ms`);
|
||||
pendingRun.reject(new Error(`Python script execution timed out after ${pendingRun.timeoutMs}ms`));
|
||||
}, pendingRun.timeoutMs);
|
||||
}
|
||||
|
||||
private finishRequest(requestId: string): void {
|
||||
if (this.activeRequestId === requestId) {
|
||||
this.activeRequestId = null;
|
||||
|
||||
@@ -76,6 +76,7 @@ export async function invokePythonApiMethodV1(method: string, args: unknown): Pr
|
||||
}
|
||||
|
||||
const normalizedArgs = asRecord(args);
|
||||
|
||||
const electronApi = getElectronApi();
|
||||
const [namespace, member] = contract.method.split('.');
|
||||
if (!namespace || !member) {
|
||||
|
||||
@@ -344,11 +344,22 @@ async function bootstrapRuntime(): Promise<void> {
|
||||
},
|
||||
});
|
||||
|
||||
runtime.globals.set('__bds_push_toast', (message: unknown, toastType?: unknown) => {
|
||||
postRuntimeMessage({
|
||||
type: 'toast',
|
||||
message: String(message ?? ''),
|
||||
...(typeof toastType === 'string' && toastType.length > 0 ? { toastType } : {}),
|
||||
});
|
||||
});
|
||||
|
||||
runtime.globals.set('__bds_api_module_source', generatePythonApiModuleV1());
|
||||
await runtime.runPythonAsync(`
|
||||
import sys
|
||||
import types
|
||||
|
||||
def toast(message, type="info"):
|
||||
__bds_push_toast(str(message), str(type))
|
||||
|
||||
__bds_api_module = types.ModuleType("bds_api")
|
||||
exec(__bds_api_module_source, __bds_api_module.__dict__)
|
||||
|
||||
|
||||
@@ -55,4 +55,5 @@ export type PythonWorkerMessage =
|
||||
| { type: 'entrypoints'; requestId: string; entrypoints: string[] }
|
||||
| { type: 'syntaxResult'; requestId: string; errors: PythonSyntaxError[] }
|
||||
| { type: 'macroResult'; requestId: string; result: MacroResultV1 }
|
||||
| { type: 'runError'; requestId: string; error: string };
|
||||
| { type: 'runError'; requestId: string; error: string }
|
||||
| { type: 'toast'; message: string; toastType?: string };
|
||||
|
||||
Reference in New Issue
Block a user