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:
@@ -39,7 +39,6 @@ const App: React.FC = () => {
|
||||
toggleSidebar,
|
||||
togglePanel,
|
||||
toggleAssistantSidebar,
|
||||
setActiveView,
|
||||
setSelectedPost,
|
||||
setActiveProject,
|
||||
setPicoTheme,
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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('#')) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }>>([]);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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')}`);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ const tabsElementSchema: z.ZodTypeAny = z.lazy(() => z.object({
|
||||
).min(1),
|
||||
}));
|
||||
|
||||
assistantPanelElementSchemaRef = z.discriminatedUnion('type', [
|
||||
assistantPanelElementSchemaRef = z.union([
|
||||
textElementSchema,
|
||||
metricElementSchema,
|
||||
listElementSchema,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
4
src/renderer/types/highlightjs-cdn-assets.d.ts
vendored
Normal file
4
src/renderer/types/highlightjs-cdn-assets.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '@highlightjs/cdn-assets/es/highlight.min.js' {
|
||||
import hljs from 'highlight.js';
|
||||
export default hljs;
|
||||
}
|
||||
Reference in New Issue
Block a user