Feature/worker threads generation (#43)

* Add worker threads architecture plan for blog generation

* fix: tries to optimize rendering, still slow

* feat: moved site rendering into web worker

* fix: calendar grabs from central data source for calendar

* fix: feeds now use blog language content and not canonical content

---------

Co-authored-by: hugo <hugoms@me.com>
This commit is contained in:
Georg Bauer
2026-03-09 22:49:25 +01:00
committed by GitHub
parent b855d61524
commit 4f9be93c6d
42 changed files with 3617 additions and 346 deletions

View File

@@ -39,7 +39,6 @@ const App: React.FC = () => {
toggleSidebar,
togglePanel,
toggleAssistantSidebar,
setActiveView,
setSelectedPost,
setActiveProject,
setPicoTheme,

View File

@@ -1,4 +1,4 @@
import React, { type ReactNode } from 'react';
import React, { type ReactElement } from 'react';
import Markdown from 'marked-react';
import type { A2UIResolvedComponent, A2UIClientAction } from '../../../main/a2ui/types';
@@ -11,7 +11,7 @@ interface A2UIComponentProps {
}
const safeRenderer = {
image(src: string, alt: string): ReactNode {
image(src: string, alt: string, _title?: string | null): ReactElement {
if (/^https?:\/\//i.test(src)) {
return <a href={src} key={src} title={alt}>{alt || src}</a>;
}

View File

@@ -1,8 +1,8 @@
import React, { type ReactNode } from 'react';
import React, { type ReactElement } from 'react';
import Markdown from 'marked-react';
import type { ChatMessage } from '../../types/electron';
import type { ChatToolEvent } from '../../navigation/useChatSurfaceState';
import type { A2UIResolvedComponent, A2UIClientAction } from '../../../main/a2ui/types';
import type { A2UIClientAction } from '../../../main/a2ui/types';
import { InlineSurface } from '../../a2ui/InlineSurface';
import type { SurfaceEntry } from '../../a2ui/useA2UISurface';
import { computeTurnIndex } from '../../a2ui/surfaceAssociation';
@@ -51,7 +51,7 @@ export const ChatTranscript: React.FC<ChatTranscriptProps> = ({
}) => {
// Block external images — CSP only allows self/data/file/blob/bds-media/bds-thumb
const safeRenderer = {
image(src: string, alt: string): ReactNode {
image(src: string, alt: string, _title?: string | null): ReactElement {
if (/^https?:\/\//i.test(src)) {
// Show alt text as a link instead of trying to load the image
return <a href={src} key={src} title={alt}>{alt || src}</a>;

View File

@@ -155,7 +155,7 @@ export const DocumentationView: React.FC<DocumentationViewProps> = ({
headingSlugCounts.set(baseId, nextCount);
const headingId = existingCount === 0 ? baseId : `${baseId}-${nextCount}`;
return React.createElement(`h${levelNumber}` as keyof JSX.IntrinsicElements, { id: headingId, key: getRendererKey('heading') }, children);
return React.createElement(`h${levelNumber}` as 'h1', { id: headingId, key: getRendererKey('heading') }, children);
},
link(href: string, text: ReactNode) {
if (!href.startsWith('#')) {

View File

@@ -5,6 +5,7 @@ import { AISuggestionsModal } from '../AISuggestionsModal/AISuggestionsModal';
import { openEntityTab } from '../../navigation/tabPolicy';
import { useI18n } from '../../i18n';
import { SUPPORTED_POST_LANGUAGES, POST_LANGUAGE_FLAGS } from '../../../main/shared/i18n';
import type { MediaData } from '../../../main/shared/electronApi';
import { getMediaDisplayName } from './editorUtils';
export const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
@@ -71,7 +72,7 @@ export const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
try {
const updated = await window.electronAPI?.media.update(item!.id, { language: newLanguage || undefined });
if (updated) {
updateMedia(item!.id, updated as Partial<typeof item>);
updateMedia(item!.id, updated as Partial<MediaData>);
}
} catch (error) {
console.error('Failed to update media language:', error);
@@ -92,7 +93,7 @@ export const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
setMediaLanguage(result.language);
const updated = await window.electronAPI?.media.update(item.id, { language: result.language });
if (updated) {
updateMedia(item.id, updated as Partial<typeof item>);
updateMedia(item.id, updated as Partial<MediaData>);
}
showToast.success(tr('editor.media.toast.languageDetected', { language: tr(`language.${result.language}`) }));
} else {
@@ -249,7 +250,7 @@ export const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
// Close AI suggestions modal
const handleCloseAISuggestionsModal = () => {
setShowAISuggestionsModal(false);
setAISuggestions(null);
setAISuggestionFields([]);
setAIError(undefined);
};
@@ -364,7 +365,7 @@ export const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
tags: tags.split(',').map(t => t.trim()).filter(t => t.length > 0),
});
if (updated) {
updateMedia(item.id, updated as Partial<typeof item>);
updateMedia(item.id, updated as Partial<MediaData>);
showToast.success(tr('editor.media.toast.updated'));
}
} catch (error) {
@@ -382,7 +383,7 @@ export const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
try {
const updated = await window.electronAPI?.media.replaceFileDialog(item.id);
if (updated) {
updateMedia(item.id, updated as Partial<typeof item>);
updateMedia(item.id, updated as Partial<MediaData>);
showToast.success(tr('editor.media.toast.fileReplaced'));
}
// null means user cancelled or file unchanged - no action needed
@@ -523,7 +524,7 @@ export const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
{item.mimeType.startsWith('image/') ? (
<div className="media-preview-image">
<img
src={`bds-media://${item.id}?t=${item.updatedAt instanceof Date ? item.updatedAt.getTime() : item.updatedAt}`}
src={`bds-media://${item.id}?t=${item.updatedAt}`}
alt={item.alt || item.originalName}
onError={(e) => {
// Fallback to placeholder if image fails to load

View File

@@ -159,7 +159,6 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
showErrorModal,
showConfirmDeleteModal,
media,
closeTab,
} = useAppStore();
// Fetch full post data from backend
@@ -194,7 +193,7 @@ export const PostEditor: React.FC<PostEditorProps> = ({ postId }) => {
const [doNotTranslate, setDoNotTranslate] = useState(false);
const [activeEditingLanguage, setActiveEditingLanguage] = useState('');
const [canonicalDraft, setCanonicalDraft] = useState<EditableContentDraft>({ title: '', excerpt: '', content: '' });
const [savedCanonicalDraft, setSavedCanonicalDraft] = useState<EditableContentDraft>({ title: '', excerpt: '', content: '' });
const [, setSavedCanonicalDraft] = useState<EditableContentDraft>({ title: '', excerpt: '', content: '' });
const [translationDrafts, setTranslationDrafts] = useState<Record<string, import('../../../main/shared/electronApi').PostTranslationData>>({});
const [savedTranslationDrafts, setSavedTranslationDrafts] = useState<Record<string, import('../../../main/shared/electronApi').PostTranslationData>>({});
const [availablePostTemplates, setAvailablePostTemplates] = useState<Array<{ slug: string; title: string }>>([]);

View File

@@ -390,7 +390,7 @@ export const GitSidebar: React.FC = () => {
recentCommitsToKeep: 2,
});
if (!result.success) {
if (result.code === 'offline') {
if ('code' in result && result.code === 'offline') {
showErrorModal({ message: tr('gitSidebar.error.offlineMode') });
return;
}

View File

@@ -208,23 +208,25 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
// Subscribe to task completion events
useEffect(() => {
const unsubscribe = window.electronAPI?.on('task:completed', (task: { taskId: string }) => {
const unsubscribe = window.electronAPI?.on('task:completed', ((...args: unknown[]) => {
const task = args[0] as { taskId: string };
setExecutionState(prev => {
if (prev.taskId !== task.taskId) return prev;
return { ...prev, isExecuting: false, completed: true };
});
});
}) as (...args: unknown[]) => void);
return () => unsubscribe?.();
}, []);
// Subscribe to task failure events
useEffect(() => {
const unsubscribe = window.electronAPI?.on('task:failed', (task: { taskId: string; error: string }) => {
const unsubscribe = window.electronAPI?.on('task:failed', ((...args: unknown[]) => {
const task = args[0] as { taskId: string; error: string };
setExecutionState(prev => {
if (prev.taskId !== task.taskId) return prev;
return { ...prev, isExecuting: false, error: task.error };
});
});
}) as (...args: unknown[]) => void);
return () => unsubscribe?.();
}, []);
@@ -919,7 +921,7 @@ const DateDistributionCard: React.FC<{ distribution: DateDistribution }> = ({ di
};
// Helper function to format post metadata for tooltip (new post from WXR)
function formatPostTooltip(wxrPost: AnalyzedPostItem['wxrPost'], t: (key: string, params?: Record<string, unknown>) => string): string {
function formatPostTooltip(wxrPost: AnalyzedPostItem['wxrPost'], t: (key: string, params?: Record<string, string | number>) => string): string {
const lines: string[] = [];
lines.push(`${t('importAnalysis.wordpressId')}: ${wxrPost.wpId}`);
lines.push(`${t('importAnalysis.type')}: ${wxrPost.postType}`);
@@ -1051,7 +1053,7 @@ function ExistingPostHoverCard({ children, className, postId }: {
}
// Helper function to format media metadata for tooltip
function formatMediaTooltip(wxrMedia: AnalyzedMediaItem['wxrMedia'], t: (key: string, params?: Record<string, unknown>) => string): string {
function formatMediaTooltip(wxrMedia: AnalyzedMediaItem['wxrMedia'], t: (key: string, params?: Record<string, string | number>) => string): string {
const lines: string[] = [];
lines.push(`${t('importAnalysis.wordpressId')}: ${wxrMedia.wpId}`);
lines.push(`${t('importAnalysis.mimeType')}: ${wxrMedia.mimeType || t('importAnalysis.unknown')}`);

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import { Editor, defaultValueCtx, editorViewCtx, rootCtx, remarkStringifyOptionsCtx, remarkPluginsCtx } from '@milkdown/kit/core';
import { commonmark, toggleStrongCommand, toggleEmphasisCommand, wrapInBlockquoteCommand, wrapInBulletListCommand, wrapInOrderedListCommand, insertHrCommand, toggleInlineCodeCommand, insertImageCommand, toggleLinkCommand } from '@milkdown/kit/preset/commonmark';
import { commonmark, toggleStrongCommand, toggleEmphasisCommand, wrapInBlockquoteCommand, wrapInBulletListCommand, wrapInOrderedListCommand, insertHrCommand, toggleInlineCodeCommand, insertImageCommand } from '@milkdown/kit/preset/commonmark';
import { gfm, toggleStrikethroughCommand } from '@milkdown/kit/preset/gfm';
import { history, undoCommand, redoCommand } from '@milkdown/kit/plugin/history';
import { listener, listenerCtx } from '@milkdown/kit/plugin/listener';

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import MonacoEditor, { type Monaco } from '@monaco-editor/react';
import type { ScriptData } from '../../../main/shared/electronApi';
import { useAppStore } from '../../store';
@@ -89,7 +89,6 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
// Refresh entrypoints asynchronously
entrypointCancelRef.current = true; // cancel any pending refresh
const cancelToken = {};
entrypointCancelRef.current = false;
const refreshEntrypoints = async () => {
try {

View File

@@ -28,7 +28,7 @@ export function SidebarEntityList<TItem>({
renderItem,
getItemKey,
topContent,
}: SidebarEntityListProps<TItem>): JSX.Element {
}: SidebarEntityListProps<TItem>): React.JSX.Element {
if (isLoading) {
return (
<div className="chat-list">

View File

@@ -346,7 +346,7 @@ export const WindowTitleBar: React.FC = () => {
};
}, [isMac, mnemonicByKey, showMnemonics]);
const handleMenuButtonClick = (event: React.MouseEvent<HTMLButtonElement>, label: string) => {
const handleMenuButtonClick = (_event: React.MouseEvent<HTMLButtonElement>, label: string) => {
const left = getMenuLeft(label);
if (left === null) {
return;

View File

@@ -1,4 +1,4 @@
import type { PythonMacroInfo, PythonMacroResolver, PythonMacroRendererFn } from './types';
import type { PythonMacroResolver, PythonMacroRendererFn } from './types';
import { setPythonMacroResolver } from './registry';
import { getPythonRuntimeManager } from '../python/runtimeManagerInstance';

View File

@@ -138,7 +138,7 @@ const tabsElementSchema: z.ZodTypeAny = z.lazy(() => z.object({
).min(1),
}));
assistantPanelElementSchemaRef = z.discriminatedUnion('type', [
assistantPanelElementSchemaRef = z.union([
textElementSchema,
metricElementSchema,
listElementSchema,

View File

@@ -1,4 +1,4 @@
import { openEntityTab } from './tabPolicy';
import { openEntityTab, type CanonicalTabSpec } from './tabPolicy';
import type { SidebarView } from './sidebarViewRegistry';
interface BlogmarkStateSnapshot {
@@ -14,7 +14,7 @@ interface BlogmarkHandlers {
setActiveView: (view: SidebarView) => void;
toggleSidebar: () => void;
setSelectedPost: (id: string) => void;
openTab: (tab: { type: 'post'; id: string; isTransient: boolean }) => void;
openTab: (tab: CanonicalTabSpec) => void;
}
export function handleBlogmarkCreatedEvent(

View File

@@ -83,13 +83,13 @@ export function parseBlogmarkCreatedEventPayload(payload: unknown): BlogmarkCrea
if (isRecord(payload.post)) {
return {
post: payload.post as PostData,
post: payload.post as unknown as PostData,
transform: parseTransformDebugInfo(payload.transform),
};
}
return {
post: payload as PostData,
post: payload as unknown as PostData,
transform: undefined,
};
}

View File

@@ -1,7 +1,7 @@
import { createPythonRuntimeWorker } from './createPythonRuntimeWorker';
import type { PythonWorkerMessage, PythonWorkerRequest } from './runtimeProtocol';
import type { PythonSyntaxError } from './runtimeProtocol';
import { parseMacroContextV1, parseMacroResultV1, type MacroContextV1, type MacroResultV1 } from './abiV1';
import { parseMacroContextV1, parseMacroResultV1, type MacroResultV1 } from './abiV1';
import { invokePythonApiMethodV1 } from './pythonApiInvokerV1';
import { showToast } from '../components/Toast';

View File

@@ -0,0 +1,4 @@
declare module '@highlightjs/cdn-assets/es/highlight.min.js' {
import hljs from 'highlight.js';
export default hljs;
}