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:
Georg Bauer
2026-03-03 14:36:15 +01:00
committed by GitHub
parent 5747925503
commit 32b66e1677
37 changed files with 2616 additions and 55 deletions

View File

@@ -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;

View File

@@ -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>

View File

@@ -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);
}