diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 013fb178..b1b50375 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -209,6 +209,8 @@ export interface DataClient { aiSearch(query: string, limit?: number): Promise; aiTasks(goal: string, context?: string): Promise; aiDownloadModel(url?: string): Promise; + /** False for clients without a transcription transport (for example the local store). */ + readonly supportsTranscription?: boolean; transcribeAsset(assetId: string, pageId: string): Promise; aiComplete(text: string, onToken: (token: string) => void, opts?: {instruction?: string; signal?: AbortSignal}): Promise; aiGenerate(prompt: string, onToken: (token: string) => void, opts?: {system?: string; maxTokens?: number; signal?: AbortSignal}): Promise; diff --git a/packages/server/src/localClient.ts b/packages/server/src/localClient.ts index 69001f19..11a37682 100644 --- a/packages/server/src/localClient.ts +++ b/packages/server/src/localClient.ts @@ -921,6 +921,8 @@ export class LocalDataClient implements DataClient { return Promise.reject(this.aiUnavailable()); } + readonly supportsTranscription = false; + transcribeAsset(): Promise { return Promise.reject(this.aiUnavailable()); } diff --git a/packages/ui/src/blockeditor/BlockEditor.tsx b/packages/ui/src/blockeditor/BlockEditor.tsx index cb8f9675..30e1539a 100644 --- a/packages/ui/src/blockeditor/BlockEditor.tsx +++ b/packages/ui/src/blockeditor/BlockEditor.tsx @@ -2584,7 +2584,10 @@ const BlockBody: React.FC = ({block, ...shared}) stays live for a reader; `pageReadOnly` is the document's real lock, which that override must not hide from a block that offers to write somewhere else. See {@link CustomBlockProps.pageReadOnly}. */} - + + {custom.type === 'meeting' && blockChildren(block) && + } + ); } diff --git a/packages/ui/src/blockeditor/MeetingBlockPlaceholder.tsx b/packages/ui/src/blockeditor/MeetingBlockPlaceholder.tsx deleted file mode 100644 index c2ee56a1..00000000 --- a/packages/ui/src/blockeditor/MeetingBlockPlaceholder.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import {blockChildren, blockProp} from './model'; -import type {CustomBlockDef, CustomBlockProps} from './registry'; - -/** MEET-5 replaces this shell. The container model already preserves notes; - * this placeholder deliberately offers no recording controls or slash entry. */ -const MeetingBlockPlaceholder = ({block}: CustomBlockProps) => ( -
- {blockProp(block, 'title') || 'Meeting'} -

{blockProp(block, 'status') || 'idle'} — Meeting view coming soon.

-

{blockChildren(block)?.length ?? 0} note blocks saved.

-
-); - -export const MEETING_BLOCK: CustomBlockDef = {type: 'meeting', render: MeetingBlockPlaceholder}; diff --git a/packages/ui/src/blockeditor/MeetingBlockView.tsx b/packages/ui/src/blockeditor/MeetingBlockView.tsx new file mode 100644 index 00000000..df1edf8a --- /dev/null +++ b/packages/ui/src/blockeditor/MeetingBlockView.tsx @@ -0,0 +1,113 @@ +import {useEffect, useMemo, useState, useSyncExternalStore} from 'react'; +import {t} from '@/i18n'; +import {assetBridge, subscribeAssetBridge} from '@/lib/assetBridge'; +import {getPageIdForDoc} from '@/lib/aiBridge'; +import {blockChildren, blockProp, insertBlock} from './model'; +import type {CustomBlockDef, CustomBlockProps} from './registry'; +import {useKitLock} from './kit/lock'; +import {KitFrame, NameDescriptionFields} from './kit/KitFrame'; +import {chunkKey, meetingRecorder, meetingTime, type MeetingAudio, type MeetingSegment} from './meetingRecorder'; + +function ChunkAudio({audio, blob}: {audio: MeetingAudio; blob?: Blob}) { + const [url, setUrl] = useState(''); + useEffect(() => { + let disposed = false; + let objectUrl = ''; + const load = async (): Promise => { + const asset = blob ? null : await assetBridge.getAsset(audio.assetId); + const data = blob ?? (asset ? new Blob([new Uint8Array(asset.bytes)], {type: asset.mime}) : null); + if (disposed || !data) return; + objectUrl = URL.createObjectURL(data); setUrl(objectUrl); + }; + void load().catch(() => {}); + return () => { disposed = true; if (objectUrl) URL.revokeObjectURL(objectUrl); }; + }, [audio.assetId, blob]); + return url ? <> +