canvas-website/src/ui/CustomToolbar.tsx

840 lines
29 KiB
TypeScript

import { TldrawUiMenuItem } from "tldraw"
import { DefaultToolbar, DefaultToolbarContent } from "tldraw"
import { useTools } from "tldraw"
import { useEditor } from "tldraw"
import { useState, useEffect, useRef, useMemo } from "react"
import { useDialogs } from "tldraw"
import { SettingsDialog } from "./SettingsDialog"
import { useAuth } from "../context/AuthContext"
import LoginButton from "../components/auth/LoginButton"
import { ObsidianVaultBrowser } from "../components/ObsidianVaultBrowser"
import { HolonBrowser } from "../components/HolonBrowser"
import { ObsNoteShape } from "../shapes/ObsNoteShapeUtil"
import { createShapeId } from "tldraw"
import type { ObsidianObsNote } from "../lib/obsidianImporter"
import { HolonData } from "../lib/HoloSphereService"
import { FathomMeetingsPanel } from "../components/FathomMeetingsPanel"
// TODO: Fix TypeScript errors in workflow files before re-enabling
// import { WorkflowPalette } from "../components/workflow/WorkflowPalette"
import { getFathomApiKey, saveFathomApiKey, removeFathomApiKey, isFathomApiKeyConfigured } from "../lib/fathomApiKey"
import { getMyConnections, updateEdgeMetadata, createConnection, removeConnection, updateTrustLevel } from "../lib/networking/connectionService"
import { TRUST_LEVEL_COLORS, type TrustLevel, type UserConnectionWithProfile, type EdgeMetadata } from "../lib/networking/types"
import { useValue } from "tldraw"
// AI tool model configurations for the dropdown
const AI_TOOLS = [
{ id: 'chat', name: 'Chat', icon: '💬', model: 'llama3.1:8b', provider: 'Ollama', type: 'local' },
{ id: 'make-real', name: 'Make Real', icon: '🔧', model: 'claude-sonnet-4-5', provider: 'Anthropic', type: 'cloud' },
{ id: 'image-gen', name: 'Image Gen', icon: '🎨', model: 'SDXL', provider: 'RunPod', type: 'gpu' },
{ id: 'video-gen', name: 'Video Gen', icon: '🎬', model: 'Wan2.1', provider: 'RunPod', type: 'gpu' },
{ id: 'transcription', name: 'Transcribe', icon: '🎤', model: 'Web Speech', provider: 'Browser', type: 'local' },
{ id: 'mycelial', name: 'Mycelial', icon: '🍄', model: 'llama3.1:70b', provider: 'Ollama', type: 'local' },
]
// Dark mode utilities
const getDarkMode = (): boolean => {
const stored = localStorage.getItem('darkMode')
if (stored !== null) {
return stored === 'true'
}
// Default to light mode instead of system preference
return false
}
const setDarkMode = (isDark: boolean) => {
localStorage.setItem('darkMode', String(isDark))
document.documentElement.classList.toggle('dark', isDark)
}
export function CustomToolbar() {
const editor = useEditor()
const tools = useTools()
const [isReady, setIsReady] = useState(false)
const [hasApiKey, setHasApiKey] = useState(false)
const { addDialog, removeDialog } = useDialogs()
const { session, setSession, clearSession } = useAuth()
const [showProfilePopup, setShowProfilePopup] = useState(false)
const [showVaultBrowser, setShowVaultBrowser] = useState(false)
const [showHolonBrowser, setShowHolonBrowser] = useState(false)
const [vaultBrowserMode, setVaultBrowserMode] = useState<'keyboard' | 'button'>('keyboard')
const [showFathomPanel, setShowFathomPanel] = useState(false)
const [showWorkflowPalette, setShowWorkflowPalette] = useState(false)
const profilePopupRef = useRef<HTMLDivElement>(null)
const [isDarkMode, setIsDarkMode] = useState(getDarkMode())
// Dropdown section states
const [expandedSection, setExpandedSection] = useState<'none' | 'ai' | 'integrations' | 'connections'>('none')
const [hasFathomApiKey, setHasFathomApiKey] = useState(false)
const [showFathomInput, setShowFathomInput] = useState(false)
const [fathomKeyInput, setFathomKeyInput] = useState('')
// Connections state
const [connections, setConnections] = useState<UserConnectionWithProfile[]>([])
const [connectionsLoading, setConnectionsLoading] = useState(false)
const [editingConnectionId, setEditingConnectionId] = useState<string | null>(null)
const [editingMetadata, setEditingMetadata] = useState<Partial<EdgeMetadata>>({})
const [savingMetadata, setSavingMetadata] = useState(false)
const [connectingUserId, setConnectingUserId] = useState<string | null>(null)
// Get collaborators from tldraw
const collaborators = useValue(
'collaborators',
() => editor.getCollaborators(),
[editor]
)
// Canvas users with their connection status
interface CanvasUser {
id: string
name: string
color: string
connectionStatus: 'trusted' | 'connected' | 'unconnected'
connectionId?: string
}
const canvasUsers: CanvasUser[] = useMemo(() => {
if (!collaborators || collaborators.length === 0) return []
return collaborators.map((c: any) => {
const userId = c.userId || c.id || c.instanceId
const connection = connections.find(conn => conn.toUserId === userId)
return {
id: userId,
name: c.userName || 'Anonymous',
color: c.color || '#888888',
connectionStatus: connection
? connection.trustLevel
: 'unconnected' as const,
connectionId: connection?.id,
}
})
}, [collaborators, connections])
// Initialize dark mode on mount
useEffect(() => {
setDarkMode(isDarkMode)
}, [])
// Check Fathom API key status
useEffect(() => {
if (session.authed && session.username) {
setHasFathomApiKey(isFathomApiKeyConfigured(session.username))
}
}, [session.authed, session.username])
// Fetch connections when section is expanded
useEffect(() => {
if (expandedSection === 'connections' && session.authed) {
setConnectionsLoading(true)
getMyConnections()
.then(setConnections)
.catch(console.error)
.finally(() => setConnectionsLoading(false))
}
}, [expandedSection, session.authed])
// Handle saving edge metadata
const handleSaveMetadata = async (connectionId: string) => {
setSavingMetadata(true)
try {
await updateEdgeMetadata(connectionId, editingMetadata)
// Refresh connections to show updated metadata
const updated = await getMyConnections()
setConnections(updated)
setEditingConnectionId(null)
setEditingMetadata({})
} catch (error) {
console.error('Failed to save metadata:', error)
} finally {
setSavingMetadata(false)
}
}
// Handle connecting to a canvas user
const handleConnect = async (userId: string, trustLevel: TrustLevel = 'connected') => {
setConnectingUserId(userId)
try {
await createConnection(userId, trustLevel)
// Refresh connections
const updated = await getMyConnections()
setConnections(updated)
} catch (error) {
console.error('Failed to connect:', error)
} finally {
setConnectingUserId(null)
}
}
// Handle disconnecting from a user
const handleDisconnect = async (connectionId: string, userId: string) => {
setConnectingUserId(userId)
try {
await removeConnection(connectionId)
// Refresh connections
const updated = await getMyConnections()
setConnections(updated)
} catch (error) {
console.error('Failed to disconnect:', error)
} finally {
setConnectingUserId(null)
}
}
// Handle changing trust level
const handleChangeTrust = async (connectionId: string, userId: string, newLevel: TrustLevel) => {
setConnectingUserId(userId)
try {
await updateTrustLevel(connectionId, newLevel)
// Refresh connections
const updated = await getMyConnections()
setConnections(updated)
} catch (error) {
console.error('Failed to update trust level:', error)
} finally {
setConnectingUserId(null)
}
}
const toggleDarkMode = () => {
const newMode = !isDarkMode
setIsDarkMode(newMode)
setDarkMode(newMode)
}
useEffect(() => {
if (editor && tools) {
setIsReady(true)
// Tools are ready
}
}, [editor, tools])
// Handle click outside profile popup
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (profilePopupRef.current && !profilePopupRef.current.contains(event.target as Node)) {
setShowProfilePopup(false)
}
}
if (showProfilePopup) {
document.addEventListener('mousedown', handleClickOutside)
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [showProfilePopup])
// Alt+O is now handled by the tool system via overrides.tsx
// It selects the ObsidianNote tool, which waits for canvas click before deploying
// Listen for open-fathom-meetings event - now creates a shape instead of modal
useEffect(() => {
const handleOpenFathomMeetings = () => {
// Allow multiple FathomMeetingsBrowser instances
// Get the current viewport center
const viewport = editor.getViewportPageBounds()
const centerX = viewport.x + viewport.w / 2
const centerY = viewport.y + viewport.h / 2
// Position new browser shape at center
const xPosition = centerX - 350 // Center the 700px wide shape
const yPosition = centerY - 300 // Center the 600px tall shape
try {
const browserShape = editor.createShape({
type: 'FathomMeetingsBrowser',
x: xPosition,
y: yPosition,
props: {
w: 700,
h: 600,
}
})
// Select the new shape and switch to select tool
editor.setSelectedShapes([`shape:${browserShape.id}`] as any)
editor.setCurrentTool('select')
} catch (error) {
console.error('❌ Error creating FathomMeetingsBrowser shape:', error)
}
}
window.addEventListener('open-fathom-meetings', handleOpenFathomMeetings)
return () => {
window.removeEventListener('open-fathom-meetings', handleOpenFathomMeetings)
}
}, [editor])
// Listen for open-obsidian-browser event - now creates a shape instead of modal
useEffect(() => {
const handleOpenBrowser = (event?: CustomEvent) => {
// Check if ObsidianBrowser already exists
const allShapes = editor.getCurrentPageShapes()
const existingBrowserShapes = allShapes.filter(shape => shape.type === 'ObsidianBrowser')
if (existingBrowserShapes.length > 0) {
// If a browser already exists, just select it
editor.setSelectedShapes([existingBrowserShapes[0].id])
editor.setCurrentTool('hand')
return
}
// No existing browser, create a new one
// Try to get click position from event or use current page point
let xPosition: number
let yPosition: number
// Check if event has click coordinates
// Standardized size: 800x600
const shapeWidth = 800
const shapeHeight = 600
const clickPoint = (event as any)?.detail?.point
if (clickPoint) {
// Use click coordinates from event
xPosition = clickPoint.x - shapeWidth / 2
yPosition = clickPoint.y - shapeHeight / 2
} else {
// Try to get current page point (if called from a click)
const currentPagePoint = editor.inputs.currentPagePoint
if (currentPagePoint && currentPagePoint.x !== undefined && currentPagePoint.y !== undefined) {
xPosition = currentPagePoint.x - shapeWidth / 2
yPosition = currentPagePoint.y - shapeHeight / 2
} else {
// Fallback to viewport center if no click coordinates available
const viewport = editor.getViewportPageBounds()
const centerX = viewport.x + viewport.w / 2
const centerY = viewport.y + viewport.h / 2
xPosition = centerX - shapeWidth / 2
yPosition = centerY - shapeHeight / 2
}
}
try {
const browserShape = editor.createShape({
type: 'ObsidianBrowser',
x: xPosition,
y: yPosition,
props: {
w: shapeWidth,
h: shapeHeight,
}
})
// Select the new shape and switch to hand tool
editor.setSelectedShapes([`shape:${browserShape.id}`] as any)
editor.setCurrentTool('hand')
} catch (error) {
console.error('❌ Error creating ObsidianBrowser shape:', error)
}
}
window.addEventListener('open-obsidian-browser', handleOpenBrowser as EventListener)
return () => {
window.removeEventListener('open-obsidian-browser', handleOpenBrowser as EventListener)
}
}, [editor])
// Listen for open-holon-browser event - now creates a shape instead of modal
useEffect(() => {
const handleOpenHolonBrowser = () => {
// Check if a HolonBrowser shape already exists
const allShapes = editor.getCurrentPageShapes()
const existingBrowserShapes = allShapes.filter(s => s.type === 'HolonBrowser')
if (existingBrowserShapes.length > 0) {
// If a browser already exists, just select it
editor.setSelectedShapes([existingBrowserShapes[0].id])
editor.setCurrentTool('select')
return
}
// Get the current viewport center
const viewport = editor.getViewportPageBounds()
const centerX = viewport.x + viewport.w / 2
const centerY = viewport.y + viewport.h / 2
// Position new browser shape at center
const xPosition = centerX - 400 // Center the 800px wide shape
const yPosition = centerY - 300 // Center the 600px tall shape
try {
const browserShape = editor.createShape({
type: 'HolonBrowser',
x: xPosition,
y: yPosition,
props: {
w: 800,
h: 600,
}
})
// Select the new shape and switch to hand tool
editor.setSelectedShapes([`shape:${browserShape.id}`] as any)
editor.setCurrentTool('hand')
} catch (error) {
console.error('❌ Error creating HolonBrowser shape:', error)
}
}
window.addEventListener('open-holon-browser', handleOpenHolonBrowser)
return () => {
window.removeEventListener('open-holon-browser', handleOpenHolonBrowser)
}
}, [editor])
// Handle Holon selection from browser
const handleHolonSelect = (holonData: HolonData) => {
try {
// Store current camera position to prevent it from changing
const currentCamera = editor.getCamera()
editor.stopCameraAnimation()
// Get the current viewport center
const viewport = editor.getViewportPageBounds()
const centerX = viewport.x + viewport.w / 2
const centerY = viewport.y + viewport.h / 2
// Standardized size: 700x400 (matches default props to fit ID and button)
const shapeWidth = 700
const shapeHeight = 400
// Position new Holon shape at viewport center
const xPosition = centerX - shapeWidth / 2
const yPosition = centerY - shapeHeight / 2
const holonShape = editor.createShape({
type: 'Holon',
x: xPosition,
y: yPosition,
props: {
w: shapeWidth,
h: shapeHeight,
name: holonData.name,
description: holonData.description || '',
latitude: holonData.latitude,
longitude: holonData.longitude,
resolution: holonData.resolution,
holonId: holonData.id,
isConnected: true,
isEditing: false,
selectedLens: 'general',
data: holonData.data,
connections: [],
lastUpdated: holonData.timestamp
}
})
// Restore camera position if it changed
const newCamera = editor.getCamera()
if (currentCamera.x !== newCamera.x || currentCamera.y !== newCamera.y || currentCamera.z !== newCamera.z) {
editor.setCamera(currentCamera, { animation: { duration: 0 } })
}
// Don't select the new shape - let it be created without selection like other tools
} catch (error) {
console.error('❌ Error creating Holon shape from data:', error)
}
}
// Listen for create-obsnote-shapes event from the tool
useEffect(() => {
const handleCreateShapes = () => {
// If vault browser is open, trigger shape creation
if (showVaultBrowser) {
const event = new CustomEvent('trigger-obsnote-creation')
window.dispatchEvent(event)
} else {
// If vault browser is not open, open it first
setVaultBrowserMode('keyboard')
setShowVaultBrowser(true)
}
}
window.addEventListener('create-obsnote-shapes', handleCreateShapes as EventListener)
return () => {
window.removeEventListener('create-obsnote-shapes', handleCreateShapes as EventListener)
}
}, [showVaultBrowser])
const checkApiKeys = () => {
const settings = localStorage.getItem("openai_api_key")
try {
if (settings) {
try {
const parsed = JSON.parse(settings)
if (parsed.keys) {
// New format with multiple providers
const hasValidKey = Object.values(parsed.keys).some(key =>
typeof key === 'string' && key.trim() !== ''
)
setHasApiKey(hasValidKey)
} else {
// Old format - single string
const hasValidKey = typeof settings === 'string' && settings.trim() !== ''
setHasApiKey(hasValidKey)
}
} catch (e) {
// Fallback to old format
const hasValidKey = typeof settings === 'string' && settings.trim() !== ''
setHasApiKey(hasValidKey)
}
} else {
setHasApiKey(false)
}
} catch (e) {
setHasApiKey(false)
}
}
// Initial check
useEffect(() => {
checkApiKeys()
}, [])
// Periodic check
useEffect(() => {
const interval = setInterval(checkApiKeys, 5000)
return () => clearInterval(interval)
}, [])
const handleLogout = () => {
// Clear the session
clearSession()
// Close the popup
setShowProfilePopup(false)
}
const handleObsNoteSelect = (obsNote: ObsidianObsNote) => {
// Get current camera position to place the obs_note
const camera = editor.getCamera()
const viewportCenter = editor.getViewportScreenCenter()
// Ensure we have valid coordinates - use camera position as fallback
const baseX = isNaN(viewportCenter.x) ? camera.x : viewportCenter.x
const baseY = isNaN(viewportCenter.y) ? camera.y : viewportCenter.y
// Get vault information from session
const vaultPath = session.obsidianVaultPath
const vaultName = session.obsidianVaultName
// Create a new obs_note shape with vault information
const obsNoteShape = ObsNoteShape.createFromObsidianObsNote(obsNote, baseX, baseY, createShapeId(), vaultPath, vaultName)
// Use the ObsNote shape directly - no conversion needed
const convertedShape = obsNoteShape
// Add the shape to the canvas
try {
// Store current camera position to prevent it from changing
const currentCamera = editor.getCamera()
editor.stopCameraAnimation()
editor.createShapes([convertedShape])
// Restore camera position if it changed
const newCamera = editor.getCamera()
if (currentCamera.x !== newCamera.x || currentCamera.y !== newCamera.y || currentCamera.z !== newCamera.z) {
editor.setCamera(currentCamera, { animation: { duration: 0 } })
}
// Select the newly created shape so user can see it
setTimeout(() => {
// Preserve camera position when selecting
const cameraBeforeSelect = editor.getCamera()
editor.stopCameraAnimation()
editor.setSelectedShapes([obsNoteShape.id])
editor.setCurrentTool('select')
// Restore camera if it changed during selection
const cameraAfterSelect = editor.getCamera()
if (cameraBeforeSelect.x !== cameraAfterSelect.x || cameraBeforeSelect.y !== cameraAfterSelect.y || cameraBeforeSelect.z !== cameraAfterSelect.z) {
editor.setCamera(cameraBeforeSelect, { animation: { duration: 0 } })
}
}, 100)
} catch (error) {
console.error('🎯 Error adding shape to canvas:', error)
}
// Close the browser
setShowVaultBrowser(false)
}
const handleObsNotesSelect = (obsNotes: ObsidianObsNote[]) => {
// Get current camera position to place the obs_notes
const camera = editor.getCamera()
const viewportCenter = editor.getViewportScreenCenter()
// Ensure we have valid coordinates - use camera position as fallback
const baseX = isNaN(viewportCenter.x) ? camera.x : viewportCenter.x
const baseY = isNaN(viewportCenter.y) ? camera.y : viewportCenter.y
// Get vault information from session
const vaultPath = session.obsidianVaultPath
const vaultName = session.obsidianVaultName
// Create obs_note shapes
const obsNoteShapes: any[] = []
for (let index = 0; index < obsNotes.length; index++) {
const obs_note = obsNotes[index]
// Use a grid-based position
const gridCols = 3
const gridWidth = 320
const gridHeight = 220
const xPosition = baseX + (index % gridCols) * gridWidth
const yPosition = baseY + Math.floor(index / gridCols) * gridHeight
const shape = ObsNoteShape.createFromObsidianObsNote(obs_note, xPosition, yPosition, createShapeId(), vaultPath, vaultName)
obsNoteShapes.push(shape)
}
// Use the ObsNote shapes directly - no conversion needed
const convertedShapes = obsNoteShapes
// Add all shapes to the canvas
try {
// Store current camera position to prevent it from changing
const currentCamera = editor.getCamera()
editor.stopCameraAnimation()
editor.createShapes(convertedShapes)
// Restore camera position if it changed
const newCamera = editor.getCamera()
if (currentCamera.x !== newCamera.x || currentCamera.y !== newCamera.y || currentCamera.z !== newCamera.z) {
editor.setCamera(currentCamera, { animation: { duration: 0 } })
}
// Select all newly created shapes so user can see them
const newShapeIds = obsNoteShapes.map(shape => shape.id)
setTimeout(() => {
// Preserve camera position when selecting
const cameraBeforeSelect = editor.getCamera()
editor.stopCameraAnimation()
editor.setSelectedShapes(newShapeIds)
editor.setCurrentTool('select')
// Restore camera if it changed during selection
const cameraAfterSelect = editor.getCamera()
if (cameraBeforeSelect.x !== cameraAfterSelect.x || cameraBeforeSelect.y !== cameraAfterSelect.y || cameraBeforeSelect.z !== cameraAfterSelect.z) {
editor.setCamera(cameraBeforeSelect, { animation: { duration: 0 } })
}
}, 100)
} catch (error) {
console.error('🎯 Error adding shapes to canvas:', error)
}
// Close the browser
setShowVaultBrowser(false)
}
if (!isReady) return null
// Only show custom tools for authenticated users
const isAuthenticated = session.authed
return (
<>
<DefaultToolbar>
<DefaultToolbarContent />
{/* Custom tools - only shown when authenticated */}
{isAuthenticated && (
<>
{tools["VideoChat"] && (
<TldrawUiMenuItem
{...tools["VideoChat"]}
icon="video"
label="Video Chat"
isSelected={tools["VideoChat"].id === editor.getCurrentToolId()}
/>
)}
{tools["ChatBox"] && (
<TldrawUiMenuItem
{...tools["ChatBox"]}
icon="chat"
label="Chat"
isSelected={tools["ChatBox"].id === editor.getCurrentToolId()}
/>
)}
{tools["Embed"] && (
<TldrawUiMenuItem
{...tools["Embed"]}
icon="embed"
label="Embed"
isSelected={tools["Embed"].id === editor.getCurrentToolId()}
/>
)}
{tools["SlideShape"] && (
<TldrawUiMenuItem
{...tools["SlideShape"]}
icon="slides"
label="Slide"
isSelected={tools["SlideShape"].id === editor.getCurrentToolId()}
/>
)}
{tools["Markdown"] && (
<TldrawUiMenuItem
{...tools["Markdown"]}
icon="markdown"
label="Markdown"
isSelected={tools["Markdown"].id === editor.getCurrentToolId()}
/>
)}
{/* MycroZine Generator temporarily disabled for debugging
{tools["MycroZineGenerator"] && (
<TldrawUiMenuItem
{...tools["MycroZineGenerator"]}
icon="zine"
label="MycroZine Generator"
isSelected={
tools["MycroZineGenerator"].id === editor.getCurrentToolId()
}
/>
)}
*/}
{tools["Prompt"] && (
<TldrawUiMenuItem
{...tools["Prompt"]}
icon="prompt"
label="LLM Prompt"
isSelected={tools["Prompt"].id === editor.getCurrentToolId()}
/>
)}
{tools["ObsidianNote"] && (
<TldrawUiMenuItem
{...tools["ObsidianNote"]}
icon="file-text"
label="Obsidian Note"
isSelected={tools["ObsidianNote"].id === editor.getCurrentToolId()}
/>
)}
{tools["Transcription"] && (
<TldrawUiMenuItem
{...tools["Transcription"]}
icon="microphone"
label="Transcription"
isSelected={tools["Transcription"].id === editor.getCurrentToolId()}
/>
)}
{/* Holon - temporarily hidden until in better working state
{tools["Holon"] && (
<TldrawUiMenuItem
{...tools["Holon"]}
icon="globe"
label="Holon"
isSelected={tools["Holon"].id === editor.getCurrentToolId()}
/>
)}
*/}
{tools["FathomMeetings"] && (
<TldrawUiMenuItem
{...tools["FathomMeetings"]}
icon="calendar"
label="Fathom Meetings"
isSelected={tools["FathomMeetings"].id === editor.getCurrentToolId()}
/>
)}
{tools["ImageGen"] && (
<TldrawUiMenuItem
{...tools["ImageGen"]}
icon="image"
label="Image Generation"
isSelected={tools["ImageGen"].id === editor.getCurrentToolId()}
/>
)}
{/* VideoGen - temporarily hidden until in better working state
{tools["VideoGen"] && (
<TldrawUiMenuItem
{...tools["VideoGen"]}
icon="video"
label="Video Generation"
isSelected={tools["VideoGen"].id === editor.getCurrentToolId()}
/>
)}
*/}
{/* Terminal (Multmux) - temporarily hidden until in better working state
{tools["Multmux"] && (
<TldrawUiMenuItem
{...tools["Multmux"]}
icon="terminal"
label="Terminal"
isSelected={tools["Multmux"].id === editor.getCurrentToolId()}
/>
)}
*/}
{tools["Map"] && (
<TldrawUiMenuItem
{...tools["Map"]}
icon="geo-globe"
label="Map"
isSelected={tools["Map"].id === editor.getCurrentToolId()}
/>
)}
{tools["calendar"] && (
<TldrawUiMenuItem
{...tools["calendar"]}
icon="calendar"
label="Calendar"
isSelected={tools["calendar"].id === editor.getCurrentToolId()}
/>
)}
{/* Workflow Builder - Toggle Palette (disabled - TypeScript errors)
<TldrawUiMenuItem
id="workflow-palette"
icon="sticker"
label="Workflow Blocks"
onSelect={() => setShowWorkflowPalette(!showWorkflowPalette)}
/>
*/}
{/* Refresh All ObsNotes Button */}
{(() => {
const allShapes = editor.getCurrentPageShapes()
const obsNoteShapes = allShapes.filter(shape => shape.type === 'ObsNote')
return obsNoteShapes.length > 0 && (
<TldrawUiMenuItem
id="refresh-all-obsnotes"
icon="refresh-cw"
label="Refresh All Notes"
onSelect={() => {
const event = new CustomEvent('refresh-all-obsnotes')
window.dispatchEvent(event)
}}
/>
)
})()}
</>
)}
</DefaultToolbar>
{/* Fathom Meetings Panel */}
{showFathomPanel && (
<FathomMeetingsPanel
onClose={() => setShowFathomPanel(false)}
/>
)}
{/* Workflow Builder Palette (disabled - TypeScript errors)
<WorkflowPalette
isOpen={showWorkflowPalette}
onClose={() => setShowWorkflowPalette(false)}
/>
*/}
</>
)
}