1084 lines
40 KiB
TypeScript
1084 lines
40 KiB
TypeScript
import { TldrawUiMenuItem } from "tldraw"
|
|
import { DefaultToolbar, DefaultToolbarContent } from "tldraw"
|
|
import { useTools } from "tldraw"
|
|
import { useEditor } from "tldraw"
|
|
import { useState, useEffect, useRef } from "react"
|
|
import { useDialogs } from "tldraw"
|
|
import { SettingsDialog } from "./SettingsDialog"
|
|
import { useAuth } from "../context/AuthContext"
|
|
import LoginButton from "../components/auth/LoginButton"
|
|
import StarBoardButton from "../components/StarBoardButton"
|
|
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"
|
|
import { LocationShareDialog } from "../components/location/LocationShareDialog"
|
|
import { getFathomApiKey, saveFathomApiKey, removeFathomApiKey, isFathomApiKeyConfigured } from "../lib/fathomApiKey"
|
|
|
|
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 [showFathomApiKeyInput, setShowFathomApiKeyInput] = useState(false)
|
|
const [fathomApiKeyInput, setFathomApiKeyInput] = useState('')
|
|
const [hasFathomApiKey, setHasFathomApiKey] = useState(false)
|
|
const profilePopupRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (editor && tools) {
|
|
setIsReady(true)
|
|
}
|
|
}, [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 = () => {
|
|
console.log('🔧 Received open-fathom-meetings event')
|
|
|
|
// Allow multiple FathomMeetingsBrowser instances - users can work with multiple meeting browsers
|
|
console.log('🔧 Creating new FathomMeetingsBrowser shape')
|
|
|
|
// 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,
|
|
}
|
|
})
|
|
|
|
console.log('✅ Created FathomMeetingsBrowser shape:', browserShape.id)
|
|
|
|
// 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) => {
|
|
console.log('🔧 Received open-obsidian-browser event')
|
|
|
|
// 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
|
|
console.log('✅ ObsidianBrowser already exists, selecting it')
|
|
editor.setSelectedShapes([existingBrowserShapes[0].id])
|
|
editor.setCurrentTool('hand')
|
|
return
|
|
}
|
|
|
|
// No existing browser, create a new one
|
|
console.log('🔧 Creating new ObsidianBrowser shape')
|
|
|
|
// 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 // Center the shape on click
|
|
yPosition = clickPoint.y - shapeHeight / 2 // Center the shape on click
|
|
console.log('📍 Positioning at event click location:', { clickPoint, xPosition, yPosition })
|
|
} 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 // Center the shape on click
|
|
yPosition = currentPagePoint.y - shapeHeight / 2 // Center the shape on click
|
|
console.log('📍 Positioning at current page point:', { currentPagePoint, xPosition, yPosition })
|
|
} 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 // Center the shape
|
|
yPosition = centerY - shapeHeight / 2 // Center the shape
|
|
console.log('📍 Positioning at viewport center (fallback):', { centerX, centerY, xPosition, yPosition })
|
|
}
|
|
}
|
|
|
|
try {
|
|
const browserShape = editor.createShape({
|
|
type: 'ObsidianBrowser',
|
|
x: xPosition,
|
|
y: yPosition,
|
|
props: {
|
|
w: shapeWidth,
|
|
h: shapeHeight,
|
|
}
|
|
})
|
|
|
|
console.log('✅ Created ObsidianBrowser shape:', browserShape.id)
|
|
|
|
// 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 = () => {
|
|
console.log('🔧 Received open-holon-browser event')
|
|
|
|
// 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
|
|
console.log('✅ HolonBrowser already exists, selecting it')
|
|
editor.setSelectedShapes([existingBrowserShapes[0].id])
|
|
editor.setCurrentTool('select')
|
|
return
|
|
}
|
|
|
|
console.log('🔧 Creating new HolonBrowser shape')
|
|
|
|
// 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,
|
|
}
|
|
})
|
|
|
|
console.log('✅ Created HolonBrowser shape:', browserShape.id)
|
|
|
|
// 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) => {
|
|
console.log('🎯 Creating Holon shape from data:', 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
|
|
}
|
|
})
|
|
|
|
console.log('✅ Created Holon shape from data:', holonShape.id)
|
|
|
|
// 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 = () => {
|
|
console.log('🎯 CustomToolbar: Received create-obsnote-shapes event')
|
|
|
|
// 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
|
|
console.log('🎯 Vault browser not open, opening 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)
|
|
}, [])
|
|
|
|
// Check Fathom API key status
|
|
useEffect(() => {
|
|
if (session.authed && session.username) {
|
|
const hasKey = isFathomApiKeyConfigured(session.username)
|
|
setHasFathomApiKey(hasKey)
|
|
} else {
|
|
setHasFathomApiKey(false)
|
|
}
|
|
}, [session.authed, session.username])
|
|
|
|
const handleLogout = () => {
|
|
// Clear the session
|
|
clearSession()
|
|
|
|
// Close the popup
|
|
setShowProfilePopup(false)
|
|
}
|
|
|
|
const openApiKeysDialog = () => {
|
|
addDialog({
|
|
id: "api-keys",
|
|
component: ({ onClose }: { onClose: () => void }) => (
|
|
<SettingsDialog
|
|
onClose={() => {
|
|
onClose()
|
|
removeDialog("api-keys")
|
|
checkApiKeys() // Refresh API key status
|
|
}}
|
|
/>
|
|
),
|
|
})
|
|
}
|
|
|
|
|
|
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
|
|
|
|
return (
|
|
<div style={{ position: "relative" }}>
|
|
<div
|
|
className="toolbar-container"
|
|
style={{
|
|
position: "fixed",
|
|
top: "4px",
|
|
right: "40px",
|
|
zIndex: 99999,
|
|
pointerEvents: "auto",
|
|
display: "flex",
|
|
gap: "6px",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<LoginButton className="toolbar-login-button" />
|
|
<StarBoardButton className="toolbar-star-button" />
|
|
|
|
{session.authed && (
|
|
<div style={{ position: "relative" }}>
|
|
<button
|
|
onClick={() => setShowProfilePopup(!showProfilePopup)}
|
|
style={{
|
|
padding: "4px 8px",
|
|
borderRadius: "4px",
|
|
background: "#6B7280",
|
|
color: "white",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
fontWeight: 500,
|
|
transition: "background 0.2s ease",
|
|
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
|
whiteSpace: "nowrap",
|
|
userSelect: "none",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "6px",
|
|
height: "22px",
|
|
minHeight: "22px",
|
|
boxSizing: "border-box",
|
|
fontSize: "0.75rem",
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.background = "#4B5563"
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.background = "#6B7280"
|
|
}}
|
|
>
|
|
<span style={{ fontSize: "12px" }}>
|
|
{hasApiKey ? "🔑" : "❌"}
|
|
</span>
|
|
<span>CryptID: {session.username}</span>
|
|
</button>
|
|
|
|
{showProfilePopup && (
|
|
<div
|
|
ref={profilePopupRef}
|
|
style={{
|
|
position: "absolute",
|
|
top: "40px",
|
|
right: "0",
|
|
width: "250px",
|
|
backgroundColor: "white",
|
|
borderRadius: "4px",
|
|
boxShadow: "0 2px 10px rgba(0,0,0,0.1)",
|
|
padding: "16px",
|
|
zIndex: 100000,
|
|
}}
|
|
>
|
|
<div style={{ marginBottom: "12px", fontWeight: "bold" }}>
|
|
CryptID: {session.username}
|
|
</div>
|
|
|
|
{/* API Key Status */}
|
|
<div style={{
|
|
marginBottom: "16px",
|
|
padding: "12px",
|
|
backgroundColor: hasApiKey ? "#f0f9ff" : "#fef2f2",
|
|
borderRadius: "4px",
|
|
border: `1px solid ${hasApiKey ? "#0ea5e9" : "#f87171"}`
|
|
}}>
|
|
<div style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
marginBottom: "8px"
|
|
}}>
|
|
<span style={{ fontWeight: "500" }}>AI API Keys</span>
|
|
<span style={{ fontSize: "14px" }}>
|
|
{hasApiKey ? "✅ Configured" : "❌ Not configured"}
|
|
</span>
|
|
</div>
|
|
<p style={{
|
|
fontSize: "12px",
|
|
color: "#666",
|
|
margin: "0 0 8px 0"
|
|
}}>
|
|
{hasApiKey
|
|
? "Your AI models are ready to use"
|
|
: "Configure API keys to use AI features"
|
|
}
|
|
</p>
|
|
<button
|
|
onClick={openApiKeysDialog}
|
|
style={{
|
|
width: "100%",
|
|
padding: "6px 12px",
|
|
backgroundColor: hasApiKey ? "#0ea5e9" : "#ef4444",
|
|
color: "white",
|
|
border: "none",
|
|
borderRadius: "4px",
|
|
cursor: "pointer",
|
|
fontSize: "12px",
|
|
fontWeight: "500",
|
|
transition: "background 0.2s",
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.backgroundColor = hasApiKey ? "#0284c7" : "#dc2626"
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.backgroundColor = hasApiKey ? "#0ea5e9" : "#ef4444"
|
|
}}
|
|
>
|
|
{hasApiKey ? "Manage Keys" : "Add API Keys"}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Obsidian Vault Settings */}
|
|
<div style={{
|
|
marginBottom: "16px",
|
|
padding: "12px",
|
|
backgroundColor: "#f8f9fa",
|
|
borderRadius: "4px",
|
|
border: "1px solid #e9ecef"
|
|
}}>
|
|
<div style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
marginBottom: "8px"
|
|
}}>
|
|
<span style={{ fontWeight: "500" }}>Obsidian Vault</span>
|
|
<span style={{ fontSize: "14px" }}>
|
|
{session.obsidianVaultName ? "✅ Configured" : "❌ Not configured"}
|
|
</span>
|
|
</div>
|
|
|
|
{session.obsidianVaultName ? (
|
|
<div style={{ marginBottom: "8px" }}>
|
|
<div style={{
|
|
fontSize: "12px",
|
|
color: "#007acc",
|
|
fontWeight: "600",
|
|
marginBottom: "4px"
|
|
}}>
|
|
{session.obsidianVaultName}
|
|
</div>
|
|
<div style={{
|
|
fontSize: "11px",
|
|
color: "#666",
|
|
fontFamily: "monospace",
|
|
wordBreak: "break-all"
|
|
}}>
|
|
{session.obsidianVaultPath === 'folder-selected'
|
|
? 'Folder selected (path not available)'
|
|
: session.obsidianVaultPath}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<p style={{
|
|
fontSize: "12px",
|
|
color: "#666",
|
|
margin: "0 0 8px 0"
|
|
}}>
|
|
No Obsidian vault configured
|
|
</p>
|
|
)}
|
|
|
|
<button
|
|
onClick={() => {
|
|
console.log('🔧 Set Vault button clicked, opening folder picker')
|
|
setVaultBrowserMode('button')
|
|
setShowVaultBrowser(true)
|
|
}}
|
|
style={{
|
|
width: "100%",
|
|
padding: "6px 12px",
|
|
backgroundColor: session.obsidianVaultName ? "#007acc" : "#28a745",
|
|
color: "white",
|
|
border: "none",
|
|
borderRadius: "4px",
|
|
cursor: "pointer",
|
|
fontSize: "12px",
|
|
fontWeight: "500",
|
|
transition: "background 0.2s",
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.backgroundColor = session.obsidianVaultName ? "#005a9e" : "#218838"
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.backgroundColor = session.obsidianVaultName ? "#007acc" : "#28a745"
|
|
}}
|
|
>
|
|
{session.obsidianVaultName ? "Change Vault" : "Set Vault"}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Fathom API Key Settings */}
|
|
<div style={{
|
|
marginBottom: "16px",
|
|
padding: "12px",
|
|
backgroundColor: hasFathomApiKey ? "#f0f9ff" : "#fef2f2",
|
|
borderRadius: "4px",
|
|
border: `1px solid ${hasFathomApiKey ? "#0ea5e9" : "#f87171"}`
|
|
}}>
|
|
<div style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
marginBottom: "8px"
|
|
}}>
|
|
<span style={{ fontWeight: "500" }}>Fathom API</span>
|
|
<span style={{ fontSize: "14px" }}>
|
|
{hasFathomApiKey ? "✅ Connected" : "❌ Not connected"}
|
|
</span>
|
|
</div>
|
|
|
|
{showFathomApiKeyInput ? (
|
|
<div>
|
|
<input
|
|
type="password"
|
|
value={fathomApiKeyInput}
|
|
onChange={(e) => setFathomApiKeyInput(e.target.value)}
|
|
placeholder="Enter Fathom API key..."
|
|
style={{
|
|
width: "100%",
|
|
padding: "6px 8px",
|
|
marginBottom: "8px",
|
|
border: "1px solid #ddd",
|
|
borderRadius: "4px",
|
|
fontSize: "12px",
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
if (fathomApiKeyInput.trim()) {
|
|
saveFathomApiKey(fathomApiKeyInput.trim(), session.username)
|
|
setHasFathomApiKey(true)
|
|
setShowFathomApiKeyInput(false)
|
|
setFathomApiKeyInput('')
|
|
}
|
|
} else if (e.key === 'Escape') {
|
|
setShowFathomApiKeyInput(false)
|
|
setFathomApiKeyInput('')
|
|
}
|
|
}}
|
|
autoFocus
|
|
/>
|
|
<div style={{ display: "flex", gap: "4px" }}>
|
|
<button
|
|
onClick={() => {
|
|
if (fathomApiKeyInput.trim()) {
|
|
saveFathomApiKey(fathomApiKeyInput.trim(), session.username)
|
|
setHasFathomApiKey(true)
|
|
setShowFathomApiKeyInput(false)
|
|
setFathomApiKeyInput('')
|
|
}
|
|
}}
|
|
style={{
|
|
flex: 1,
|
|
padding: "4px 8px",
|
|
backgroundColor: "#0ea5e9",
|
|
color: "white",
|
|
border: "none",
|
|
borderRadius: "4px",
|
|
cursor: "pointer",
|
|
fontSize: "11px",
|
|
}}
|
|
>
|
|
Save
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setShowFathomApiKeyInput(false)
|
|
setFathomApiKeyInput('')
|
|
}}
|
|
style={{
|
|
flex: 1,
|
|
padding: "4px 8px",
|
|
backgroundColor: "#6b7280",
|
|
color: "white",
|
|
border: "none",
|
|
borderRadius: "4px",
|
|
cursor: "pointer",
|
|
fontSize: "11px",
|
|
}}
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<p style={{
|
|
fontSize: "12px",
|
|
color: "#666",
|
|
margin: "0 0 8px 0"
|
|
}}>
|
|
{hasFathomApiKey
|
|
? "Your Fathom account is connected"
|
|
: "Connect your Fathom account to import meetings"}
|
|
</p>
|
|
<div style={{ display: "flex", gap: "4px" }}>
|
|
<button
|
|
onClick={() => {
|
|
setShowFathomApiKeyInput(true)
|
|
const currentKey = getFathomApiKey(session.username)
|
|
if (currentKey) {
|
|
setFathomApiKeyInput(currentKey)
|
|
}
|
|
}}
|
|
style={{
|
|
flex: 1,
|
|
padding: "6px 12px",
|
|
backgroundColor: hasFathomApiKey ? "#0ea5e9" : "#ef4444",
|
|
color: "white",
|
|
border: "none",
|
|
borderRadius: "4px",
|
|
cursor: "pointer",
|
|
fontSize: "12px",
|
|
fontWeight: "500",
|
|
}}
|
|
>
|
|
{hasFathomApiKey ? "Change Key" : "Add API Key"}
|
|
</button>
|
|
{hasFathomApiKey && (
|
|
<button
|
|
onClick={() => {
|
|
removeFathomApiKey(session.username)
|
|
setHasFathomApiKey(false)
|
|
}}
|
|
style={{
|
|
padding: "6px 12px",
|
|
backgroundColor: "#6b7280",
|
|
color: "white",
|
|
border: "none",
|
|
borderRadius: "4px",
|
|
cursor: "pointer",
|
|
fontSize: "12px",
|
|
fontWeight: "500",
|
|
}}
|
|
>
|
|
Disconnect
|
|
</button>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<a
|
|
href="/dashboard"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{
|
|
display: "block",
|
|
width: "100%",
|
|
padding: "8px 12px",
|
|
backgroundColor: "#3B82F6",
|
|
color: "white",
|
|
border: "none",
|
|
borderRadius: "4px",
|
|
cursor: "pointer",
|
|
fontWeight: "500",
|
|
textDecoration: "none",
|
|
textAlign: "center",
|
|
marginBottom: "8px",
|
|
transition: "background 0.2s",
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.backgroundColor = "#2563EB"
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.backgroundColor = "#3B82F6"
|
|
}}
|
|
>
|
|
My Dashboard
|
|
</a>
|
|
|
|
{!session.backupCreated && (
|
|
<div style={{
|
|
marginBottom: "12px",
|
|
fontSize: "12px",
|
|
color: "#666",
|
|
padding: "8px",
|
|
backgroundColor: "#f8f8f8",
|
|
borderRadius: "4px"
|
|
}}>
|
|
Remember to back up your encryption keys to prevent data loss!
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
onClick={handleLogout}
|
|
style={{
|
|
width: "100%",
|
|
padding: "8px 12px",
|
|
backgroundColor: "#EF4444",
|
|
color: "white",
|
|
border: "none",
|
|
borderRadius: "4px",
|
|
cursor: "pointer",
|
|
fontWeight: "500",
|
|
transition: "background 0.2s",
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.backgroundColor = "#DC2626"
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.backgroundColor = "#EF4444"
|
|
}}
|
|
>
|
|
Sign Out
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<DefaultToolbar>
|
|
<DefaultToolbarContent />
|
|
{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()}
|
|
/>
|
|
)}
|
|
{tools["MycrozineTemplate"] && (
|
|
<TldrawUiMenuItem
|
|
{...tools["MycrozineTemplate"]}
|
|
icon="mycrozinetemplate"
|
|
label="MycrozineTemplate"
|
|
isSelected={
|
|
tools["MycrozineTemplate"].id === editor.getCurrentToolId()
|
|
}
|
|
/>
|
|
)}
|
|
{tools["Prompt"] && (
|
|
<TldrawUiMenuItem
|
|
{...tools["Prompt"]}
|
|
icon="prompt"
|
|
label="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()}
|
|
/>
|
|
)}
|
|
{tools["Holon"] && (
|
|
<TldrawUiMenuItem
|
|
{...tools["Holon"]}
|
|
icon="globe"
|
|
label="Holon"
|
|
isSelected={tools["Holon"].id === editor.getCurrentToolId()}
|
|
/>
|
|
)}
|
|
{tools["ImageGen"] && (
|
|
<TldrawUiMenuItem
|
|
{...tools["ImageGen"]}
|
|
icon="image"
|
|
label="Image Generation"
|
|
isSelected={tools["ImageGen"].id === editor.getCurrentToolId()}
|
|
/>
|
|
)}
|
|
{/* Share Location tool removed for now */}
|
|
{/* 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)}
|
|
/>
|
|
)}
|
|
|
|
</div>
|
|
)
|
|
}
|