diff --git a/src/automerge/AutomergeToTLStore.ts b/src/automerge/AutomergeToTLStore.ts index 49b3307..023b710 100644 --- a/src/automerge/AutomergeToTLStore.ts +++ b/src/automerge/AutomergeToTLStore.ts @@ -682,6 +682,7 @@ export function sanitizeRecord(record: any): TLRecord { 'holon': 'Holon', 'obsidianBrowser': 'ObsidianBrowser', 'fathomMeetingsBrowser': 'FathomMeetingsBrowser', + 'meetingIntelligenceBrowser': 'MeetingIntelligenceBrowser', 'imageGen': 'ImageGen', 'videoGen': 'VideoGen', 'multmux': 'Multmux', diff --git a/src/automerge/useAutomergeStoreV2.ts b/src/automerge/useAutomergeStoreV2.ts index 1477425..4fdb2e9 100644 --- a/src/automerge/useAutomergeStoreV2.ts +++ b/src/automerge/useAutomergeStoreV2.ts @@ -40,6 +40,7 @@ const CUSTOM_SHAPE_TYPES = [ 'Holon', 'ObsidianBrowser', 'FathomMeetingsBrowser', + 'MeetingIntelligenceBrowser', 'ImageGen', 'VideoGen', 'Multmux', @@ -167,6 +168,7 @@ import { FathomNoteShape } from "@/shapes/FathomNoteShapeUtil" import { HolonShape } from "@/shapes/HolonShapeUtil" import { ObsidianBrowserShape } from "@/shapes/ObsidianBrowserShapeUtil" import { FathomMeetingsBrowserShape } from "@/shapes/FathomMeetingsBrowserShapeUtil" +import { MeetingIntelligenceBrowserShape } from "@/shapes/MeetingIntelligenceBrowserShapeUtil" import { ImageGenShape } from "@/shapes/ImageGenShapeUtil" import { VideoGenShape } from "@/shapes/VideoGenShapeUtil" import { MultmuxShape } from "@/shapes/MultmuxShapeUtil" @@ -218,6 +220,7 @@ export function useAutomergeStoreV2({ HolonShape, ObsidianBrowserShape, FathomMeetingsBrowserShape, + MeetingIntelligenceBrowserShape, ImageGenShape, VideoGenShape, MultmuxShape, diff --git a/src/components/MeetingIntelligencePanel.tsx b/src/components/MeetingIntelligencePanel.tsx new file mode 100644 index 0000000..e8ce7fa --- /dev/null +++ b/src/components/MeetingIntelligencePanel.tsx @@ -0,0 +1,262 @@ +import React, { useState, useEffect } from 'react' +import { useEditor } from 'tldraw' + +/** Shape of a meeting from the Meeting Intelligence API */ +interface MIMeeting { + id: string + title: string + status: string + duration_seconds?: number + participants?: string[] + created_at: string + has_transcript?: boolean + has_summary?: boolean + speaker_count?: number +} + +interface MeetingIntelligencePanelProps { + onClose?: () => void + onMeetingSelect?: ( + meeting: MIMeeting, + options: { summary: boolean; transcript: boolean; speakers: boolean }, + ) => void + shapeMode?: boolean +} + +// Meeting Intelligence API base URL +const MI_API_URL = 'https://meets-api.rspace.online' + +function formatDuration(seconds?: number): string { + if (!seconds) return '—' + const h = Math.floor(seconds / 3600) + const m = Math.floor((seconds % 3600) / 60) + if (h > 0) return `${h}h ${m}m` + return `${m}m` +} + +function formatDate(dateStr: string): string { + try { + const d = new Date(dateStr) + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + } catch { + return dateStr + } +} + +function statusColor(status: string): string { + switch (status) { + case 'completed': return '#10b981' + case 'recording': return '#ef4444' + case 'transcribing': case 'diarizing': return '#f59e0b' + case 'summarizing': return '#8b5cf6' + default: return '#6b7280' + } +} + +export function MeetingIntelligencePanel({ onMeetingSelect }: MeetingIntelligencePanelProps) { + const editor = useEditor() + const [meetings, setMeetings] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const fetchMeetings = async () => { + setLoading(true) + setError(null) + try { + const res = await fetch(`${MI_API_URL}/meetings`) + if (!res.ok) throw new Error(`API returned ${res.status}`) + const data = await res.json() as any + // API may return { meetings: [...] } or [...] + const list = Array.isArray(data) ? data : (data.meetings || []) + setMeetings(list) + } catch (err: any) { + setError(err.message || 'Failed to load meetings') + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchMeetings() + }, []) + + const panelStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + height: '100%', + fontFamily: 'Inter, -apple-system, sans-serif', + fontSize: '13px', + color: '#1a1a1a', + backgroundColor: '#fff', + } + + const headerStyle: React.CSSProperties = { + padding: '12px 16px', + borderBottom: '1px solid #e5e7eb', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: '8px', + } + + const listStyle: React.CSSProperties = { + flex: 1, + overflow: 'auto', + padding: '8px', + } + + const cardStyle: React.CSSProperties = { + padding: '12px', + marginBottom: '8px', + borderRadius: '8px', + border: '1px solid #e5e7eb', + backgroundColor: '#fafafa', + cursor: 'default', + } + + const badgeStyle = (color: string): React.CSSProperties => ({ + display: 'inline-block', + padding: '2px 8px', + borderRadius: '9999px', + fontSize: '11px', + fontWeight: 600, + color: '#fff', + backgroundColor: color, + }) + + const btnStyle = (color: string): React.CSSProperties => ({ + padding: '4px 10px', + borderRadius: '6px', + border: 'none', + fontSize: '11px', + fontWeight: 600, + color: '#fff', + backgroundColor: color, + cursor: 'pointer', + transition: 'opacity 0.15s', + }) + + return ( +
+ {/* Header */} +
+ Meeting Intelligence + +
+ + {/* Content */} +
+ {loading && ( +
+ Loading meetings... +
+ )} + + {error && ( +
+ {error} +
+ +
+ )} + + {!loading && !error && meetings.length === 0 && ( +
+ No meetings found +
+ )} + + {meetings.map((m) => ( +
+ {/* Meeting title + status */} +
+ + {m.title || 'Untitled Meeting'} + + + {m.status} + +
+ + {/* Meta row */} +
+ {formatDate(m.created_at)} + {formatDuration(m.duration_seconds)} + {m.speaker_count != null && {m.speaker_count} speakers} +
+ + {/* Pull buttons */} +
+ {m.has_summary && ( + + )} + {m.has_transcript && ( + + )} + {(m.speaker_count ?? 0) > 0 && ( + + )} + {/* Pull All — only if multiple types available */} + {(m.has_summary || m.has_transcript) && ( + + )} +
+
+ ))} +
+
+ ) +} diff --git a/src/lib/activityLogger.ts b/src/lib/activityLogger.ts index 313ba78..039ed46 100644 --- a/src/lib/activityLogger.ts +++ b/src/lib/activityLogger.ts @@ -48,6 +48,7 @@ const SHAPE_DISPLAY_NAMES: Record = { 'HolonBrowser': 'holon browser', 'ObsidianBrowser': 'Obsidian browser', 'FathomMeetingsBrowser': 'Fathom browser', + 'MeetingIntelligenceBrowser': 'Meeting Intelligence browser', 'FathomNote': 'Fathom note', 'ImageGen': 'AI image', 'VideoGen': 'AI video', diff --git a/src/routes/Board.tsx b/src/routes/Board.tsx index 4f209a7..d6fac09 100644 --- a/src/routes/Board.tsx +++ b/src/routes/Board.tsx @@ -43,6 +43,8 @@ import { HolonBrowserShape } from "@/shapes/HolonBrowserShapeUtil" import { ObsidianBrowserShape } from "@/shapes/ObsidianBrowserShapeUtil" import { FathomMeetingsBrowserShape } from "@/shapes/FathomMeetingsBrowserShapeUtil" import { FathomNoteShape } from "@/shapes/FathomNoteShapeUtil" +import { MeetingIntelligenceBrowserShape } from "@/shapes/MeetingIntelligenceBrowserShapeUtil" +import { MeetingIntelligenceTool } from "@/tools/MeetingIntelligenceTool" import { ImageGenShape } from "@/shapes/ImageGenShapeUtil" import { ImageGenTool } from "@/tools/ImageGenTool" import { VideoGenShape } from "@/shapes/VideoGenShapeUtil" @@ -162,6 +164,7 @@ const customShapeUtils = [ ObsidianBrowserShape, FathomMeetingsBrowserShape, FathomNoteShape, // Individual Fathom meeting notes created from FathomMeetingsBrowser + MeetingIntelligenceBrowserShape, // Self-hosted meeting intelligence browser ImageGenShape, VideoGenShape, BlenderGenShape, // Blender 3D procedural generation @@ -190,6 +193,7 @@ const customTools = [ TranscriptionTool, HolonTool, FathomMeetingsTool, + MeetingIntelligenceTool, // Self-hosted meeting intelligence tool ImageGenTool, VideoGenTool, BlenderGenTool, // Blender 3D procedural generation @@ -1269,9 +1273,10 @@ export function Board() { // Check if any selected shapes are browser shapes that should not be deleted const selectedShapes = editor.getSelectedShapes(); const hasBrowserShape = selectedShapes.some(shape => - shape.type === 'ObsidianBrowser' || - shape.type === 'HolonBrowser' || - shape.type === 'FathomMeetingsBrowser' + shape.type === 'ObsidianBrowser' || + shape.type === 'HolonBrowser' || + shape.type === 'FathomMeetingsBrowser' || + shape.type === 'MeetingIntelligenceBrowser' ); // Prevent deletion of browser shapes with Escape diff --git a/src/shapes/MeetingIntelligenceBrowserShapeUtil.tsx b/src/shapes/MeetingIntelligenceBrowserShapeUtil.tsx new file mode 100644 index 0000000..0593112 --- /dev/null +++ b/src/shapes/MeetingIntelligenceBrowserShapeUtil.tsx @@ -0,0 +1,337 @@ +import { + BaseBoxShapeUtil, + HTMLContainer, + TLBaseShape, + createShapeId, + Box, + IndexKey, + TLParentId, +} from "tldraw" +import React, { useState } from "react" +import { MeetingIntelligencePanel } from "../components/MeetingIntelligencePanel" +import { StandardizedToolWrapper } from "../components/StandardizedToolWrapper" +import { usePinnedToView } from "../hooks/usePinnedToView" +import { useMaximize } from "../hooks/useMaximize" +import { FathomNoteShape } from "./FathomNoteShapeUtil" + +// Meeting Intelligence API base URL +const MI_API_URL = 'https://meets-api.rspace.online' + +type IMeetingIntelligenceBrowser = TLBaseShape< + "MeetingIntelligenceBrowser", + { + w: number + h: number + pinnedToView: boolean + tags: string[] + } +> + +export class MeetingIntelligenceBrowserShape extends BaseBoxShapeUtil { + static override type = "MeetingIntelligenceBrowser" as const + + getDefaultProps(): IMeetingIntelligenceBrowser["props"] { + return { + w: 800, + h: 600, + pinnedToView: false, + tags: ['meeting-intelligence', 'meetings', 'transcription'], + } + } + + // Meeting Intelligence theme: purple + static readonly PRIMARY_COLOR = "#8b5cf6" + + component(shape: IMeetingIntelligenceBrowser) { + const { w, h } = shape.props + const [isOpen, setIsOpen] = useState(true) + const [isMinimized, setIsMinimized] = useState(false) + const isSelected = this.editor.getSelectedShapeIds().includes(shape.id) + + usePinnedToView(this.editor, shape.id, shape.props.pinnedToView) + + const { isMaximized, toggleMaximize } = useMaximize({ + editor: this.editor, + shapeId: shape.id, + currentW: w, + currentH: h, + shapeType: 'MeetingIntelligenceBrowser', + }) + + const handleClose = () => { + setIsOpen(false) + this.editor.deleteShape(shape.id) + } + + const handleMinimize = () => { + setIsMinimized(!isMinimized) + } + + const handlePinToggle = () => { + this.editor.updateShape({ + id: shape.id, + type: shape.type, + props: { + ...shape.props, + pinnedToView: !shape.props.pinnedToView, + }, + }) + } + + const handleMeetingSelect = async ( + meeting: any, + options: { summary: boolean; transcript: boolean; speakers: boolean }, + ) => { + try { + const meetingId = meeting.id + if (!meetingId) { + console.error('No meeting ID found:', meeting) + return + } + + const meetingTitle = meeting.title || 'Untitled Meeting' + + // Helper to format date + const formatDate = (dateStr?: string): string => { + if (!dateStr) return '' + try { + const d = new Date(dateStr) + return `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}` + } catch { return '' } + } + const meetingDate = formatDate(meeting.created_at) + + // Fetch requested data in parallel + const [summaryRes, transcriptRes, speakersRes]: [any, any, any] = await Promise.all([ + options.summary + ? fetch(`${MI_API_URL}/meetings/${meetingId}/summary`).then(r => r.ok ? r.json() : null).catch(() => null) + : Promise.resolve(null), + options.transcript + ? fetch(`${MI_API_URL}/meetings/${meetingId}/transcript`).then(r => r.ok ? r.json() : null).catch(() => null) + : Promise.resolve(null), + options.speakers + ? fetch(`${MI_API_URL}/meetings/${meetingId}/speakers`).then(r => r.ok ? r.json() : null).catch(() => null) + : Promise.resolve(null), + ]) + + // Position notes next to the browser + const browserBounds = this.editor.getShapePageBounds(shape.id) + const spacing = 30 + let startX = browserBounds ? browserBounds.x + browserBounds.w + spacing : 0 + const startY = browserBounds ? browserBounds.y : 0 + + const shapeWidth = 500 + const shapeHeight = 600 + const hSpacing = 20 + + // Find existing MI shapes for this meeting to avoid overlap + const allShapes = this.editor.getCurrentPageShapes() + const existingForMeeting = allShapes.filter(s => { + if (s.type !== 'FathomNote') return false + const noteId = (s as any).props?.noteId || '' + return noteId.includes(`mi-${meetingId}`) + }) + if (existingForMeeting.length > 0) { + let rightmost = startX + existingForMeeting.forEach(s => { + const b = this.editor.getShapePageBounds(s.id) + if (b) rightmost = Math.max(rightmost, b.x + b.w) + }) + startX = rightmost + hSpacing + } + + let currentX = startX + const shapesToCreate: any[] = [] + + // Summary note + if (summaryRes) { + const summaryText = summaryRes.summary_text || summaryRes.summary || summaryRes.overview || '' + const actionItems = summaryRes.action_items || [] + const decisions = summaryRes.decisions || [] + const keyPoints = summaryRes.key_points || [] + + let content = '' + if (meetingDate) { + content += `
+

${meetingTitle}: Summary

+ ${meetingDate} +
\n\n` + } else { + content += `# ${meetingTitle}: Summary\n\n` + } + if (summaryText) content += `${summaryText}\n\n` + if (keyPoints.length > 0) { + content += `## Key Points\n${keyPoints.map((p: any) => `- ${typeof p === 'string' ? p : p.text || p.point || ''}`).join('\n')}\n\n` + } + if (decisions.length > 0) { + content += `## Decisions\n${decisions.map((d: any) => `- ${typeof d === 'string' ? d : d.text || d.decision || ''}`).join('\n')}\n\n` + } + if (actionItems.length > 0) { + content += `## Action Items\n${actionItems.map((a: any) => `- [ ] ${a.task || a.text || a.description || ''}`).join('\n')}\n\n` + } + + shapesToCreate.push({ + ...FathomNoteShape.createFromData( + { id: `mi-summary-${meetingId}`, title: 'Meeting Summary', content, tags: ['meeting-intelligence', 'summary'], primaryColor: '#8b5cf6' }, + currentX, startY + ), + props: { + ...FathomNoteShape.createFromData( + { id: `mi-summary-${meetingId}`, title: 'Meeting Summary', content, tags: ['meeting-intelligence', 'summary'], primaryColor: '#8b5cf6' }, + currentX, startY + ).props, + w: shapeWidth, h: shapeHeight, + }, + }) + currentX += shapeWidth + hSpacing + } + + // Transcript note + if (transcriptRes) { + const segments = Array.isArray(transcriptRes) ? transcriptRes : (transcriptRes.segments || transcriptRes.transcript || []) + + let content = '' + if (meetingDate) { + content += `
+

${meetingTitle}: Transcript

+ ${meetingDate} +
\n\n` + } else { + content += `# ${meetingTitle}: Transcript\n\n` + } + + if (segments.length > 0) { + content += segments.map((seg: any) => { + const speaker = seg.speaker_label || seg.speaker || seg.name || 'Unknown' + const text = seg.text || seg.content || '' + const start = seg.start_time != null ? `${Math.floor(seg.start_time / 60)}:${String(Math.floor(seg.start_time % 60)).padStart(2, '0')}` : '' + return start ? `**${speaker}** (${start}): ${text}` : `**${speaker}**: ${text}` + }).join('\n\n') + } else { + content += '*No transcript segments available.*' + } + + shapesToCreate.push({ + ...FathomNoteShape.createFromData( + { id: `mi-transcript-${meetingId}`, title: 'Meeting Transcript', content, tags: ['meeting-intelligence', 'transcript'], primaryColor: '#2563eb' }, + currentX, startY + ), + props: { + ...FathomNoteShape.createFromData( + { id: `mi-transcript-${meetingId}`, title: 'Meeting Transcript', content, tags: ['meeting-intelligence', 'transcript'], primaryColor: '#2563eb' }, + currentX, startY + ).props, + w: shapeWidth, h: shapeHeight, + }, + }) + currentX += shapeWidth + hSpacing + } + + // Speakers note + if (speakersRes) { + const speakers = Array.isArray(speakersRes) ? speakersRes : (speakersRes.speakers || []) + + let content = '' + if (meetingDate) { + content += `
+

${meetingTitle}: Speakers

+ ${meetingDate} +
\n\n` + } else { + content += `# ${meetingTitle}: Speakers\n\n` + } + + if (speakers.length > 0) { + content += speakers.map((s: any) => { + const name = s.speaker_label || s.name || s.label || 'Unknown' + const time = s.speaking_time || s.speaking_time_seconds || 0 + const mins = Math.floor(time / 60) + const secs = Math.floor(time % 60) + const segments = s.segment_count || s.segments || '—' + return `### ${name}\n- Speaking time: ${mins}m ${secs}s\n- Segments: ${segments}` + }).join('\n\n') + } else { + content += '*No speaker data available.*' + } + + shapesToCreate.push({ + ...FathomNoteShape.createFromData( + { id: `mi-speakers-${meetingId}`, title: 'Meeting Speakers', content, tags: ['meeting-intelligence', 'speakers'], primaryColor: '#059669' }, + currentX, startY + ), + props: { + ...FathomNoteShape.createFromData( + { id: `mi-speakers-${meetingId}`, title: 'Meeting Speakers', content, tags: ['meeting-intelligence', 'speakers'], primaryColor: '#059669' }, + currentX, startY + ).props, + w: shapeWidth, h: shapeHeight, + }, + }) + } + + // Create all shapes at once + if (shapesToCreate.length > 0) { + this.editor.createShapes(shapesToCreate) + + // Zoom to show results + setTimeout(() => { + const firstBounds = this.editor.getShapePageBounds(shapesToCreate[0].id) + if (firstBounds && browserBounds) { + const combined = Box.Common([browserBounds, firstBounds]) + this.editor.zoomToBounds(combined, { + inset: 50, + animation: { duration: 500, easing: (t) => t * (2 - t) }, + }) + } + this.editor.setSelectedShapes([shapesToCreate[0].id] as any) + this.editor.setCurrentTool('select') + }, 50) + } + } catch (error) { + console.error('Error creating Meeting Intelligence shapes:', error) + } + } + + if (!isOpen) return null + + return ( + + { + this.editor.updateShape({ + id: shape.id, + type: 'MeetingIntelligenceBrowser', + props: { ...shape.props, tags: newTags }, + }) + }} + tagsEditable={true} + > + + + + ) + } + + indicator(shape: IMeetingIntelligenceBrowser) { + return + } +} diff --git a/src/tools/MeetingIntelligenceTool.ts b/src/tools/MeetingIntelligenceTool.ts new file mode 100644 index 0000000..8973e75 --- /dev/null +++ b/src/tools/MeetingIntelligenceTool.ts @@ -0,0 +1,148 @@ +import { StateNode } from "tldraw" + +export class MeetingIntelligenceTool extends StateNode { + static override id = "meeting-intelligence" + static override initial = "idle" + static override children = () => [MeetingIntelligenceIdle] +} + +export class MeetingIntelligenceIdle extends StateNode { + static override id = "idle" + + tooltipElement?: HTMLDivElement + mouseMoveHandler?: (e: MouseEvent) => void + isCreatingShape = false + + override onEnter = () => { + this.editor.setCursor({ type: "cross", rotation: 0 }) + + this.tooltipElement = document.createElement('div') + this.tooltipElement.style.cssText = ` + position: fixed; + background: rgba(0, 0, 0, 0.85); + color: white; + padding: 8px 12px; + border-radius: 6px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 13px; + white-space: nowrap; + z-index: 10000; + pointer-events: none; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + ` + this.tooltipElement.textContent = 'Click anywhere to place Meeting Intelligence browser' + document.body.appendChild(this.tooltipElement) + + this.mouseMoveHandler = (e: MouseEvent) => { + if (this.tooltipElement) { + let x = e.clientX + 15 + let y = e.clientY - 35 + const rect = this.tooltipElement.getBoundingClientRect() + if (x + rect.width > window.innerWidth) x = e.clientX - rect.width - 15 + if (y + rect.height > window.innerHeight) y = e.clientY - rect.height - 15 + x = Math.max(10, x) + y = Math.max(10, y) + this.tooltipElement.style.left = `${x}px` + this.tooltipElement.style.top = `${y}px` + } + } + document.addEventListener('mousemove', this.mouseMoveHandler) + } + + override onPointerDown = (info?: any) => { + if (this.isCreatingShape) return + if (!info || !info.point || info.button === undefined) return + if (info.button !== 0) return + + if (info.target && typeof info.target === 'object') { + const target = info.target as HTMLElement + if (target.closest('[data-tldraw-ui]') || + target.closest('.tlui-menu') || + target.closest('.tlui-toolbar') || + target.closest('[role="menu"]') || + target.closest('[role="toolbar"]')) { + return + } + } + + let clickX: number | undefined + let clickY: number | undefined + + if (info.point) { + try { + const pagePoint = this.editor.screenToPage(info.point) + clickX = pagePoint.x + clickY = pagePoint.y + } catch (e) { + console.error('MeetingIntelligenceTool: Failed to convert point', e) + } + } + + if (clickX === undefined || clickY === undefined) return + if (clickX === 0 && clickY === 0) return + + const currentTool = this.editor.getCurrentToolId() + if (currentTool !== 'meeting-intelligence') return + + this.createShape(clickX, clickY) + } + + override onExit = () => { + this.cleanupTooltip() + this.isCreatingShape = false + } + + private cleanupTooltip = () => { + if (this.mouseMoveHandler) { + document.removeEventListener('mousemove', this.mouseMoveHandler) + this.mouseMoveHandler = undefined + } + if (this.tooltipElement) { + document.body.removeChild(this.tooltipElement) + this.tooltipElement = undefined + } + } + + private createShape(clickX: number, clickY: number) { + this.isCreatingShape = true + try { + const currentCamera = this.editor.getCamera() + this.editor.stopCameraAnimation() + + const shapeWidth = 800 + const shapeHeight = 600 + const finalX = clickX - shapeWidth / 2 + const finalY = clickY - shapeHeight / 2 + + const browserShape = this.editor.createShape({ + type: 'MeetingIntelligenceBrowser', + x: finalX, + y: finalY, + props: { w: shapeWidth, h: shapeHeight }, + }) + + const newCamera = this.editor.getCamera() + if (currentCamera.x !== newCamera.x || currentCamera.y !== newCamera.y || currentCamera.z !== newCamera.z) { + this.editor.setCamera(currentCamera, { animation: { duration: 0 } }) + } + + const shapeId = browserShape.id.startsWith('shape:') ? browserShape.id : `shape:${browserShape.id}` + const camBefore = this.editor.getCamera() + this.editor.stopCameraAnimation() + this.editor.setSelectedShapes([shapeId] as any) + this.editor.setCurrentTool('select') + + const camAfter = this.editor.getCamera() + if (camBefore.x !== camAfter.x || camBefore.y !== camAfter.y || camBefore.z !== camAfter.z) { + this.editor.setCamera(camBefore, { animation: { duration: 0 } }) + } + + setTimeout(() => { this.isCreatingShape = false }, 200) + } catch (error) { + console.error('Error creating MeetingIntelligenceBrowser shape:', error) + this.isCreatingShape = false + throw error + } + } +} diff --git a/src/ui/CustomMainMenu.tsx b/src/ui/CustomMainMenu.tsx index bf60f00..8511184 100644 --- a/src/ui/CustomMainMenu.tsx +++ b/src/ui/CustomMainMenu.tsx @@ -31,7 +31,7 @@ export function CustomMainMenu() { const validateAndNormalizeShapeType = (shape: any): string => { if (!shape || !shape.type) return 'text' - const validCustomShapes = ['ObsNote', 'VideoChat', 'Transcription', 'Prompt', 'ChatBox', 'Embed', 'Markdown', 'MycrozineTemplate', 'Slide', 'Holon', 'ObsidianBrowser', 'HolonBrowser', 'FathomMeetingsBrowser', 'ImageGen', 'VideoGen', 'Multmux', 'FathomNote', 'GoogleItem', 'Map', 'PrivateWorkspace', 'SharedPiano', 'Drawfast', 'MycelialIntelligence'] + const validCustomShapes = ['ObsNote', 'VideoChat', 'Transcription', 'Prompt', 'ChatBox', 'Embed', 'Markdown', 'MycrozineTemplate', 'Slide', 'Holon', 'ObsidianBrowser', 'HolonBrowser', 'FathomMeetingsBrowser', 'MeetingIntelligenceBrowser', 'ImageGen', 'VideoGen', 'Multmux', 'FathomNote', 'GoogleItem', 'Map', 'PrivateWorkspace', 'SharedPiano', 'Drawfast', 'MycelialIntelligence'] const validDefaultShapes = ['arrow', 'bookmark', 'draw', 'embed', 'frame', 'geo', 'group', 'highlight', 'image', 'line', 'note', 'text', 'video'] const allValidShapes = [...validCustomShapes, ...validDefaultShapes] diff --git a/src/ui/overrides.tsx b/src/ui/overrides.tsx index 2a73228..719a17a 100644 --- a/src/ui/overrides.tsx +++ b/src/ui/overrides.tsx @@ -211,6 +211,13 @@ export const overrides: TLUiOverrides = { // Shape creation is handled manually in FathomMeetingsTool.onPointerDown onSelect: () => editor.setCurrentTool("fathom-meetings"), }, + MeetingIntelligence: { + id: "meeting-intelligence", + icon: "microphone", + label: "Meeting Intelligence", + readonlyOk: true, + onSelect: () => editor.setCurrentTool("meeting-intelligence"), + }, ImageGen: { id: "ImageGen", icon: "image", diff --git a/src/utils/shapeCollisionUtils.ts b/src/utils/shapeCollisionUtils.ts index f406f3d..11003fa 100644 --- a/src/utils/shapeCollisionUtils.ts +++ b/src/utils/shapeCollisionUtils.ts @@ -32,11 +32,11 @@ export function resolveOverlaps(editor: Editor, shapeId: string): void { const allShapes = editor.getCurrentPageShapes() const customShapeTypes = [ 'ObsNote', 'ObsidianBrowser', 'HolonBrowser', 'VideoChat', - 'Transcription', 'Holon', 'FathomMeetingsBrowser', 'Prompt', + 'Transcription', 'Holon', 'FathomMeetingsBrowser', 'MeetingIntelligenceBrowser', 'Prompt', 'Embed', 'Slide', 'Markdown', 'MycrozineTemplate', 'ChatBox', 'ImageGen', 'VideoGen', 'Multmux' ] - + const shape = editor.getShape(shapeId as TLShapeId) if (!shape || !customShapeTypes.includes(shape.type as string)) return @@ -120,11 +120,11 @@ export function findNonOverlappingPosition( const allShapes = editor.getCurrentPageShapes() const customShapeTypes = [ 'ObsNote', 'ObsidianBrowser', 'HolonBrowser', 'VideoChat', - 'Transcription', 'Holon', 'FathomMeetingsBrowser', 'Prompt', + 'Transcription', 'Holon', 'FathomMeetingsBrowser', 'MeetingIntelligenceBrowser', 'Prompt', 'Embed', 'Slide', 'Markdown', 'MycrozineTemplate', 'ChatBox', 'ImageGen', 'VideoGen', 'Multmux' ] - + const existingShapes = allShapes.filter( s => !excludeShapeIds.includes(s.id) && customShapeTypes.includes(s.type) )