feat: add IO Chip tool for visual I/O routing between canvas tools
- Add IOChipShapeUtil with frame-like container and pin system - Add IOChipTool for drawing IO chips on canvas - Add IOChipTemplateService for saving/loading chip templates - Add ChipTemplateBrowser component for template management - Add wiring system with type-compatible pin connections - Add IO chip CSS styles - Register IOChip in Board.tsx, overrides.tsx, and useAutomergeStoreV2.ts - Keyboard shortcuts: Alt+Shift+P (tool), Alt+T (template browser) Features: - Auto-analyze contained shapes to generate input/output pins - Visual wiring between pins with SVG curved connections - Save chips as reusable templates with categories - Built-in templates for common AI pipelines - Pin type compatibility checking for connections 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a8c3988e3f
commit
527462acb7
|
|
@ -127,6 +127,7 @@ import { FathomMeetingsBrowserShape } from "@/shapes/FathomMeetingsBrowserShapeU
|
||||||
import { ImageGenShape } from "@/shapes/ImageGenShapeUtil"
|
import { ImageGenShape } from "@/shapes/ImageGenShapeUtil"
|
||||||
import { VideoGenShape } from "@/shapes/VideoGenShapeUtil"
|
import { VideoGenShape } from "@/shapes/VideoGenShapeUtil"
|
||||||
import { MultmuxShape } from "@/shapes/MultmuxShapeUtil"
|
import { MultmuxShape } from "@/shapes/MultmuxShapeUtil"
|
||||||
|
import { IOChipShape } from "@/shapes/IOChipShapeUtil"
|
||||||
// Location shape removed - no longer needed
|
// Location shape removed - no longer needed
|
||||||
|
|
||||||
export function useAutomergeStoreV2({
|
export function useAutomergeStoreV2({
|
||||||
|
|
@ -160,6 +161,7 @@ export function useAutomergeStoreV2({
|
||||||
ImageGen: {} as any,
|
ImageGen: {} as any,
|
||||||
VideoGen: {} as any,
|
VideoGen: {} as any,
|
||||||
Multmux: {} as any,
|
Multmux: {} as any,
|
||||||
|
IOChip: {} as any,
|
||||||
},
|
},
|
||||||
bindings: defaultBindingSchemas,
|
bindings: defaultBindingSchemas,
|
||||||
})
|
})
|
||||||
|
|
@ -184,6 +186,7 @@ export function useAutomergeStoreV2({
|
||||||
ImageGenShape,
|
ImageGenShape,
|
||||||
VideoGenShape,
|
VideoGenShape,
|
||||||
MultmuxShape,
|
MultmuxShape,
|
||||||
|
IOChipShape,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
return store
|
return store
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,455 @@
|
||||||
|
import React, { useState, useEffect, useMemo } from 'react'
|
||||||
|
import { Editor, createShapeId } from 'tldraw'
|
||||||
|
import { ioChipTemplateService, IOChipTemplate, IOChipCategory } from '@/lib/IOChipTemplateService'
|
||||||
|
import { PIN_TYPE_ICONS, PIN_TYPE_COLORS, IOPinType } from '@/shapes/IOChipShapeUtil'
|
||||||
|
|
||||||
|
interface ChipTemplateBrowserProps {
|
||||||
|
editor: Editor
|
||||||
|
onClose: () => void
|
||||||
|
position?: { x: number; y: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChipTemplateBrowser({ editor, onClose, position }: ChipTemplateBrowserProps) {
|
||||||
|
const [templates, setTemplates] = useState<IOChipTemplate[]>([])
|
||||||
|
const [categories, setCategories] = useState<IOChipCategory[]>([])
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [selectedTemplate, setSelectedTemplate] = useState<IOChipTemplate | null>(null)
|
||||||
|
|
||||||
|
// Load templates and subscribe to changes
|
||||||
|
useEffect(() => {
|
||||||
|
const loadData = () => {
|
||||||
|
setTemplates(ioChipTemplateService.getAllTemplates())
|
||||||
|
setCategories(ioChipTemplateService.getCategories())
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData()
|
||||||
|
const unsubscribe = ioChipTemplateService.subscribe(loadData)
|
||||||
|
return unsubscribe
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Filter templates based on search and category
|
||||||
|
const filteredTemplates = useMemo(() => {
|
||||||
|
let result = templates
|
||||||
|
|
||||||
|
if (selectedCategory) {
|
||||||
|
result = result.filter(t => t.category === selectedCategory)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery.trim()) {
|
||||||
|
result = ioChipTemplateService.searchTemplates(searchQuery)
|
||||||
|
if (selectedCategory) {
|
||||||
|
result = result.filter(t => t.category === selectedCategory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}, [templates, selectedCategory, searchQuery])
|
||||||
|
|
||||||
|
// Create chip from template
|
||||||
|
const handleCreateFromTemplate = (template: IOChipTemplate) => {
|
||||||
|
const viewport = editor.getViewportPageBounds()
|
||||||
|
const x = position?.x ?? (viewport.x + viewport.w / 2 - template.width / 2)
|
||||||
|
const y = position?.y ?? (viewport.y + viewport.h / 2 - template.height / 2)
|
||||||
|
|
||||||
|
// Create the IO chip shape from template
|
||||||
|
editor.createShape({
|
||||||
|
id: createShapeId(),
|
||||||
|
type: 'IOChip',
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
props: {
|
||||||
|
w: template.width,
|
||||||
|
h: template.height,
|
||||||
|
name: template.name,
|
||||||
|
description: template.description,
|
||||||
|
inputPins: template.inputPins,
|
||||||
|
outputPins: template.outputPins,
|
||||||
|
wires: template.wires,
|
||||||
|
containedShapeIds: [],
|
||||||
|
isAnalyzing: false,
|
||||||
|
lastAnalyzed: Date.now(),
|
||||||
|
pinnedToView: false,
|
||||||
|
tags: template.tags,
|
||||||
|
autoAnalyze: true,
|
||||||
|
showPinLabels: true,
|
||||||
|
templateId: template.id,
|
||||||
|
category: template.category,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Recreate contained shapes from template
|
||||||
|
// This would require storing and restoring shape definitions
|
||||||
|
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete template
|
||||||
|
const handleDeleteTemplate = (templateId: string, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (confirm('Are you sure you want to delete this template?')) {
|
||||||
|
ioChipTemplateService.deleteTemplate(templateId)
|
||||||
|
if (selectedTemplate?.id === templateId) {
|
||||||
|
setSelectedTemplate(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export template
|
||||||
|
const handleExportTemplate = (template: IOChipTemplate, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
const json = ioChipTemplateService.exportTemplate(template.id)
|
||||||
|
if (json) {
|
||||||
|
const blob = new Blob([json], { type: 'application/json' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `${template.name.toLowerCase().replace(/\s+/g, '-')}.iochip.json`
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render pin preview
|
||||||
|
const renderPinPreview = (pins: { type: IOPinType }[], direction: 'input' | 'output') => {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', gap: '2px', flexWrap: 'wrap' }}>
|
||||||
|
{pins.slice(0, 5).map((pin, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
fontSize: '10px',
|
||||||
|
padding: '1px 4px',
|
||||||
|
backgroundColor: `${PIN_TYPE_COLORS[pin.type]}20`,
|
||||||
|
color: PIN_TYPE_COLORS[pin.type],
|
||||||
|
borderRadius: '3px',
|
||||||
|
}}
|
||||||
|
title={pin.type}
|
||||||
|
>
|
||||||
|
{PIN_TYPE_ICONS[pin.type]}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{pins.length > 5 && (
|
||||||
|
<span style={{ fontSize: '10px', color: '#94a3b8' }}>+{pins.length - 5}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: '600px',
|
||||||
|
maxHeight: '80vh',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
|
||||||
|
zIndex: 10000,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '16px 20px',
|
||||||
|
borderBottom: '1px solid #e2e8f0',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
backgroundColor: '#f8fafc',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 style={{ margin: 0, fontSize: '18px', fontWeight: 600, color: '#1e293b' }}>
|
||||||
|
🔌 IO Chip Templates
|
||||||
|
</h2>
|
||||||
|
<p style={{ margin: '4px 0 0', fontSize: '12px', color: '#64748b' }}>
|
||||||
|
Select a template to create a new IO chip
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: '#f1f5f9',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '18px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search and filters */}
|
||||||
|
<div style={{ padding: '12px 20px', borderBottom: '1px solid #e2e8f0' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Search templates..."
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px 14px',
|
||||||
|
border: '1px solid #e2e8f0',
|
||||||
|
borderRadius: '8px',
|
||||||
|
fontSize: '14px',
|
||||||
|
marginBottom: '12px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Category filters */}
|
||||||
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedCategory(null)}
|
||||||
|
style={{
|
||||||
|
padding: '6px 12px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: selectedCategory === null ? '#3b82f6' : '#f1f5f9',
|
||||||
|
color: selectedCategory === null ? 'white' : '#64748b',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
onClick={() => setSelectedCategory(cat.id)}
|
||||||
|
style={{
|
||||||
|
padding: '6px 12px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: selectedCategory === cat.id ? '#3b82f6' : '#f1f5f9',
|
||||||
|
color: selectedCategory === cat.id ? 'white' : '#64748b',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cat.icon} {cat.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Template grid */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '16px 20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filteredTemplates.length === 0 ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '40px',
|
||||||
|
color: '#94a3b8',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '48px', marginBottom: '12px' }}>📦</div>
|
||||||
|
<div style={{ fontSize: '14px' }}>
|
||||||
|
{searchQuery ? 'No templates match your search' : 'No templates yet'}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', marginTop: '8px' }}>
|
||||||
|
Create an IO chip and save it as a template
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||||
|
gap: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filteredTemplates.map((template) => (
|
||||||
|
<div
|
||||||
|
key={template.id}
|
||||||
|
onClick={() => setSelectedTemplate(template)}
|
||||||
|
style={{
|
||||||
|
padding: '14px',
|
||||||
|
borderRadius: '10px',
|
||||||
|
border: selectedTemplate?.id === template.id
|
||||||
|
? '2px solid #3b82f6'
|
||||||
|
: '1px solid #e2e8f0',
|
||||||
|
backgroundColor: selectedTemplate?.id === template.id
|
||||||
|
? '#eff6ff'
|
||||||
|
: 'white',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (selectedTemplate?.id !== template.id) {
|
||||||
|
e.currentTarget.style.borderColor = '#94a3b8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (selectedTemplate?.id !== template.id) {
|
||||||
|
e.currentTarget.style.borderColor = '#e2e8f0'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: '14px', color: '#1e293b' }}>
|
||||||
|
{template.icon || '🔌'} {template.name}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleExportTemplate(template, e)}
|
||||||
|
style={{
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: '#f1f5f9',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '12px',
|
||||||
|
}}
|
||||||
|
title="Export template"
|
||||||
|
>
|
||||||
|
📤
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleDeleteTemplate(template.id, e)}
|
||||||
|
style={{
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: '#fef2f2',
|
||||||
|
color: '#ef4444',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '12px',
|
||||||
|
}}
|
||||||
|
title="Delete template"
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{template.description && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#64748b',
|
||||||
|
marginBottom: '10px',
|
||||||
|
lineHeight: 1.4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{template.description.slice(0, 80)}
|
||||||
|
{template.description.length > 80 ? '...' : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '16px', marginBottom: '8px' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '10px', color: '#94a3b8', marginBottom: '4px' }}>
|
||||||
|
Inputs
|
||||||
|
</div>
|
||||||
|
{renderPinPreview(template.inputPins, 'input')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '10px', color: '#94a3b8', marginBottom: '4px' }}>
|
||||||
|
Outputs
|
||||||
|
</div>
|
||||||
|
{renderPinPreview(template.outputPins, 'output')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
|
||||||
|
{template.tags.slice(0, 3).map((tag, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
fontSize: '10px',
|
||||||
|
padding: '2px 6px',
|
||||||
|
backgroundColor: '#f1f5f9',
|
||||||
|
borderRadius: '4px',
|
||||||
|
color: '#64748b',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer with actions */}
|
||||||
|
{selectedTemplate && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '16px 20px',
|
||||||
|
borderTop: '1px solid #e2e8f0',
|
||||||
|
backgroundColor: '#f8fafc',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '13px', color: '#64748b' }}>
|
||||||
|
Selected: <strong>{selectedTemplate.name}</strong>
|
||||||
|
<span style={{ marginLeft: '8px', fontSize: '11px' }}>
|
||||||
|
({selectedTemplate.inputPins.length} inputs, {selectedTemplate.outputPins.length} outputs)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCreateFromTemplate(selectedTemplate)}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create IO Chip
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook to manage template browser visibility
|
||||||
|
export function useChipTemplateBrowser() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [position, setPosition] = useState<{ x: number; y: number } | undefined>()
|
||||||
|
|
||||||
|
const open = (pos?: { x: number; y: number }) => {
|
||||||
|
setPosition(pos)
|
||||||
|
setIsOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
setIsOpen(false)
|
||||||
|
setPosition(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isOpen, position, open, close }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
/* IO Chip Styles */
|
||||||
|
|
||||||
|
/* Wire animation */
|
||||||
|
@keyframes wire-flow {
|
||||||
|
from {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: -20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.io-chip-wire-animated path {
|
||||||
|
animation: wire-flow 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pin hover effects */
|
||||||
|
.io-chip-pin {
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.io-chip-pin:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wiring mode cursor */
|
||||||
|
.io-chip-wiring-mode {
|
||||||
|
cursor: crosshair !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.io-chip-wiring-mode * {
|
||||||
|
cursor: crosshair !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Template browser scrollbar */
|
||||||
|
.io-chip-template-browser::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.io-chip-template-browser::-webkit-scrollbar-track {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.io-chip-template-browser::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.io-chip-template-browser::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pin type badges */
|
||||||
|
.io-pin-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.io-pin-badge-text { background: #dbeafe; color: #1d4ed8; }
|
||||||
|
.io-pin-badge-image { background: #ede9fe; color: #7c3aed; }
|
||||||
|
.io-pin-badge-video { background: #fce7f3; color: #db2777; }
|
||||||
|
.io-pin-badge-url { background: #cffafe; color: #0891b2; }
|
||||||
|
.io-pin-badge-file { background: #fef3c7; color: #d97706; }
|
||||||
|
.io-pin-badge-identity { background: #d1fae5; color: #059669; }
|
||||||
|
.io-pin-badge-api { background: #fee2e2; color: #dc2626; }
|
||||||
|
.io-pin-badge-shape { background: #e0e7ff; color: #4f46e5; }
|
||||||
|
.io-pin-badge-data { background: #ecfccb; color: #65a30d; }
|
||||||
|
.io-pin-badge-prompt { background: #ffedd5; color: #ea580c; }
|
||||||
|
.io-pin-badge-embedding { background: #ccfbf1; color: #0d9488; }
|
||||||
|
.io-pin-badge-stream { background: #e0f2fe; color: #0284c7; }
|
||||||
|
|
||||||
|
/* Template card hover effect */
|
||||||
|
.io-chip-template-card {
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.io-chip-template-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Save dialog animation */
|
||||||
|
@keyframes dialog-appear {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.io-chip-save-dialog {
|
||||||
|
animation: dialog-appear 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wire connection indicator */
|
||||||
|
.io-chip-wire-indicator {
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #10b981;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pulse 1.5s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chip container drop zone */
|
||||||
|
.io-chip-drop-zone {
|
||||||
|
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.io-chip-drop-zone.active {
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
background-color: #eff6ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
.dark .io-chip-template-browser {
|
||||||
|
background: #1e293b;
|
||||||
|
border-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .io-chip-template-card {
|
||||||
|
background: #0f172a;
|
||||||
|
border-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .io-chip-template-card:hover {
|
||||||
|
border-color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .io-pin-label {
|
||||||
|
background: rgba(30, 41, 59, 0.9);
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,364 @@
|
||||||
|
import { TLShapeId } from "tldraw"
|
||||||
|
import { IOPin, IOPinType } from "@/shapes/IOChipShapeUtil"
|
||||||
|
|
||||||
|
// Wire connection between two pins
|
||||||
|
export interface IOWireConnection {
|
||||||
|
id: string
|
||||||
|
fromPinId: string
|
||||||
|
toPinId: string
|
||||||
|
fromShapeId: TLShapeId
|
||||||
|
toShapeId: TLShapeId
|
||||||
|
pinType: IOPinType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contained shape reference with relative position
|
||||||
|
export interface ContainedShapeRef {
|
||||||
|
originalId: TLShapeId
|
||||||
|
type: string
|
||||||
|
relativeX: number // Position relative to chip origin
|
||||||
|
relativeY: number
|
||||||
|
props: Record<string, any> // Sanitized props (no sensitive data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full chip template schema
|
||||||
|
export interface IOChipTemplate {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
category?: string
|
||||||
|
icon?: string
|
||||||
|
createdAt: number
|
||||||
|
updatedAt: number
|
||||||
|
|
||||||
|
// Chip dimensions
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
|
||||||
|
// I/O schema
|
||||||
|
inputPins: IOPin[]
|
||||||
|
outputPins: IOPin[]
|
||||||
|
|
||||||
|
// Internal structure
|
||||||
|
containedShapes: ContainedShapeRef[]
|
||||||
|
wires: IOWireConnection[]
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
tags: string[]
|
||||||
|
author?: string
|
||||||
|
version?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template category for organization
|
||||||
|
export interface IOChipCategory {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default categories
|
||||||
|
export const DEFAULT_CATEGORIES: IOChipCategory[] = [
|
||||||
|
{ id: 'ai', name: 'AI & ML', icon: '🤖', description: 'AI and machine learning pipelines' },
|
||||||
|
{ id: 'media', name: 'Media', icon: '🎬', description: 'Image, video, and audio processing' },
|
||||||
|
{ id: 'data', name: 'Data', icon: '📊', description: 'Data transformation and analysis' },
|
||||||
|
{ id: 'integration', name: 'Integration', icon: '🔗', description: 'API and service integrations' },
|
||||||
|
{ id: 'utility', name: 'Utility', icon: '🔧', description: 'General purpose utilities' },
|
||||||
|
{ id: 'custom', name: 'Custom', icon: '⭐', description: 'User-created templates' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Storage key
|
||||||
|
const TEMPLATES_STORAGE_KEY = 'io-chip-templates'
|
||||||
|
const CATEGORIES_STORAGE_KEY = 'io-chip-categories'
|
||||||
|
|
||||||
|
class IOChipTemplateService {
|
||||||
|
private templates: Map<string, IOChipTemplate> = new Map()
|
||||||
|
private categories: IOChipCategory[] = [...DEFAULT_CATEGORIES]
|
||||||
|
private listeners: Set<() => void> = new Set()
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.loadFromStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load templates from localStorage
|
||||||
|
private loadFromStorage(): void {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(TEMPLATES_STORAGE_KEY)
|
||||||
|
if (stored) {
|
||||||
|
const parsed = JSON.parse(stored) as IOChipTemplate[]
|
||||||
|
this.templates = new Map(parsed.map(t => [t.id, t]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedCategories = localStorage.getItem(CATEGORIES_STORAGE_KEY)
|
||||||
|
if (storedCategories) {
|
||||||
|
this.categories = JSON.parse(storedCategories)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to load IO chip templates:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save templates to localStorage
|
||||||
|
private saveToStorage(): void {
|
||||||
|
try {
|
||||||
|
const templates = Array.from(this.templates.values())
|
||||||
|
localStorage.setItem(TEMPLATES_STORAGE_KEY, JSON.stringify(templates))
|
||||||
|
localStorage.setItem(CATEGORIES_STORAGE_KEY, JSON.stringify(this.categories))
|
||||||
|
this.notifyListeners()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to save IO chip templates:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to changes
|
||||||
|
subscribe(listener: () => void): () => void {
|
||||||
|
this.listeners.add(listener)
|
||||||
|
return () => this.listeners.delete(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyListeners(): void {
|
||||||
|
this.listeners.forEach(listener => listener())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique ID
|
||||||
|
private generateId(): string {
|
||||||
|
return `chip-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save a new template
|
||||||
|
saveTemplate(template: Omit<IOChipTemplate, 'id' | 'createdAt' | 'updatedAt'>): IOChipTemplate {
|
||||||
|
const now = Date.now()
|
||||||
|
const newTemplate: IOChipTemplate = {
|
||||||
|
...template,
|
||||||
|
id: this.generateId(),
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.templates.set(newTemplate.id, newTemplate)
|
||||||
|
this.saveToStorage()
|
||||||
|
|
||||||
|
console.log('💾 Saved IO chip template:', newTemplate.name)
|
||||||
|
return newTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing template
|
||||||
|
updateTemplate(id: string, updates: Partial<IOChipTemplate>): IOChipTemplate | null {
|
||||||
|
const existing = this.templates.get(id)
|
||||||
|
if (!existing) {
|
||||||
|
console.error('❌ Template not found:', id)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated: IOChipTemplate = {
|
||||||
|
...existing,
|
||||||
|
...updates,
|
||||||
|
id, // Preserve ID
|
||||||
|
createdAt: existing.createdAt, // Preserve creation time
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.templates.set(id, updated)
|
||||||
|
this.saveToStorage()
|
||||||
|
|
||||||
|
console.log('📝 Updated IO chip template:', updated.name)
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete template
|
||||||
|
deleteTemplate(id: string): boolean {
|
||||||
|
const deleted = this.templates.delete(id)
|
||||||
|
if (deleted) {
|
||||||
|
this.saveToStorage()
|
||||||
|
console.log('🗑️ Deleted IO chip template:', id)
|
||||||
|
}
|
||||||
|
return deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get single template
|
||||||
|
getTemplate(id: string): IOChipTemplate | undefined {
|
||||||
|
return this.templates.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all templates
|
||||||
|
getAllTemplates(): IOChipTemplate[] {
|
||||||
|
return Array.from(this.templates.values()).sort((a, b) => b.updatedAt - a.updatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get templates by category
|
||||||
|
getTemplatesByCategory(categoryId: string): IOChipTemplate[] {
|
||||||
|
return this.getAllTemplates().filter(t => t.category === categoryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search templates
|
||||||
|
searchTemplates(query: string): IOChipTemplate[] {
|
||||||
|
const lowerQuery = query.toLowerCase()
|
||||||
|
return this.getAllTemplates().filter(t =>
|
||||||
|
t.name.toLowerCase().includes(lowerQuery) ||
|
||||||
|
t.description?.toLowerCase().includes(lowerQuery) ||
|
||||||
|
t.tags.some(tag => tag.toLowerCase().includes(lowerQuery))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all categories
|
||||||
|
getCategories(): IOChipCategory[] {
|
||||||
|
return this.categories
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom category
|
||||||
|
addCategory(category: Omit<IOChipCategory, 'id'>): IOChipCategory {
|
||||||
|
const newCategory: IOChipCategory = {
|
||||||
|
...category,
|
||||||
|
id: `cat-${Date.now()}`,
|
||||||
|
}
|
||||||
|
this.categories.push(newCategory)
|
||||||
|
this.saveToStorage()
|
||||||
|
return newCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export template as JSON
|
||||||
|
exportTemplate(id: string): string | null {
|
||||||
|
const template = this.templates.get(id)
|
||||||
|
if (!template) return null
|
||||||
|
return JSON.stringify(template, null, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import template from JSON
|
||||||
|
importTemplate(json: string): IOChipTemplate | null {
|
||||||
|
try {
|
||||||
|
const template = JSON.parse(json) as IOChipTemplate
|
||||||
|
// Generate new ID to avoid conflicts
|
||||||
|
template.id = this.generateId()
|
||||||
|
template.createdAt = Date.now()
|
||||||
|
template.updatedAt = Date.now()
|
||||||
|
|
||||||
|
this.templates.set(template.id, template)
|
||||||
|
this.saveToStorage()
|
||||||
|
|
||||||
|
console.log('📥 Imported IO chip template:', template.name)
|
||||||
|
return template
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to import template:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export all templates
|
||||||
|
exportAllTemplates(): string {
|
||||||
|
return JSON.stringify(this.getAllTemplates(), null, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import multiple templates
|
||||||
|
importTemplates(json: string): number {
|
||||||
|
try {
|
||||||
|
const templates = JSON.parse(json) as IOChipTemplate[]
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
for (const template of templates) {
|
||||||
|
template.id = this.generateId()
|
||||||
|
template.createdAt = Date.now()
|
||||||
|
template.updatedAt = Date.now()
|
||||||
|
this.templates.set(template.id, template)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveToStorage()
|
||||||
|
console.log(`📥 Imported ${count} IO chip templates`)
|
||||||
|
return count
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to import templates:', error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create some built-in example templates
|
||||||
|
createBuiltInTemplates(): void {
|
||||||
|
if (this.templates.size > 0) return // Don't overwrite existing
|
||||||
|
|
||||||
|
// Image Generation Pipeline
|
||||||
|
this.saveTemplate({
|
||||||
|
name: 'Text to Image',
|
||||||
|
description: 'Generate images from text prompts using AI',
|
||||||
|
category: 'ai',
|
||||||
|
icon: '🎨',
|
||||||
|
width: 500,
|
||||||
|
height: 300,
|
||||||
|
inputPins: [
|
||||||
|
{ id: 'prompt-in', name: 'Prompt', type: 'prompt', direction: 'input', required: true },
|
||||||
|
{ id: 'style-in', name: 'Style', type: 'text', direction: 'input', required: false },
|
||||||
|
],
|
||||||
|
outputPins: [
|
||||||
|
{ id: 'image-out', name: 'Generated Image', type: 'image', direction: 'output' },
|
||||||
|
],
|
||||||
|
containedShapes: [],
|
||||||
|
wires: [],
|
||||||
|
tags: ['ai', 'image', 'generation', 'stable-diffusion'],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Video Generation Pipeline
|
||||||
|
this.saveTemplate({
|
||||||
|
name: 'Image to Video',
|
||||||
|
description: 'Animate images into videos using AI',
|
||||||
|
category: 'ai',
|
||||||
|
icon: '🎬',
|
||||||
|
width: 600,
|
||||||
|
height: 350,
|
||||||
|
inputPins: [
|
||||||
|
{ id: 'image-in', name: 'Source Image', type: 'image', direction: 'input', required: true },
|
||||||
|
{ id: 'prompt-in', name: 'Motion Prompt', type: 'prompt', direction: 'input', required: false },
|
||||||
|
],
|
||||||
|
outputPins: [
|
||||||
|
{ id: 'video-out', name: 'Generated Video', type: 'video', direction: 'output' },
|
||||||
|
],
|
||||||
|
containedShapes: [],
|
||||||
|
wires: [],
|
||||||
|
tags: ['ai', 'video', 'animation', 'wan'],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Chat Pipeline
|
||||||
|
this.saveTemplate({
|
||||||
|
name: 'AI Chat',
|
||||||
|
description: 'Conversational AI with context',
|
||||||
|
category: 'ai',
|
||||||
|
icon: '💬',
|
||||||
|
width: 450,
|
||||||
|
height: 400,
|
||||||
|
inputPins: [
|
||||||
|
{ id: 'message-in', name: 'User Message', type: 'text', direction: 'input', required: true },
|
||||||
|
{ id: 'context-in', name: 'Context', type: 'data', direction: 'input', required: false },
|
||||||
|
],
|
||||||
|
outputPins: [
|
||||||
|
{ id: 'response-out', name: 'AI Response', type: 'text', direction: 'output' },
|
||||||
|
],
|
||||||
|
containedShapes: [],
|
||||||
|
wires: [],
|
||||||
|
tags: ['ai', 'chat', 'llm', 'conversation'],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Transcription Pipeline
|
||||||
|
this.saveTemplate({
|
||||||
|
name: 'Audio Transcription',
|
||||||
|
description: 'Convert speech to text',
|
||||||
|
category: 'media',
|
||||||
|
icon: '🎤',
|
||||||
|
width: 400,
|
||||||
|
height: 250,
|
||||||
|
inputPins: [
|
||||||
|
{ id: 'audio-in', name: 'Audio File', type: 'file', direction: 'input', required: true },
|
||||||
|
],
|
||||||
|
outputPins: [
|
||||||
|
{ id: 'transcript-out', name: 'Transcript', type: 'text', direction: 'output' },
|
||||||
|
],
|
||||||
|
containedShapes: [],
|
||||||
|
wires: [],
|
||||||
|
tags: ['audio', 'transcription', 'speech-to-text', 'whisper'],
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('📦 Created built-in IO chip templates')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
export const ioChipTemplateService = new IOChipTemplateService()
|
||||||
|
|
||||||
|
// Initialize built-in templates
|
||||||
|
ioChipTemplateService.createBuiltInTemplates()
|
||||||
|
|
@ -46,6 +46,8 @@ import { VideoGenShape } from "@/shapes/VideoGenShapeUtil"
|
||||||
import { VideoGenTool } from "@/tools/VideoGenTool"
|
import { VideoGenTool } from "@/tools/VideoGenTool"
|
||||||
import { MultmuxTool } from "@/tools/MultmuxTool"
|
import { MultmuxTool } from "@/tools/MultmuxTool"
|
||||||
import { MultmuxShape } from "@/shapes/MultmuxShapeUtil"
|
import { MultmuxShape } from "@/shapes/MultmuxShapeUtil"
|
||||||
|
import { IOChipTool } from "@/tools/IOChipTool"
|
||||||
|
import { IOChipShape } from "@/shapes/IOChipShapeUtil"
|
||||||
import {
|
import {
|
||||||
lockElement,
|
lockElement,
|
||||||
unlockElement,
|
unlockElement,
|
||||||
|
|
@ -62,6 +64,7 @@ import { CmdK } from "@/CmdK"
|
||||||
import "react-cmdk/dist/cmdk.css"
|
import "react-cmdk/dist/cmdk.css"
|
||||||
import "@/css/style.css"
|
import "@/css/style.css"
|
||||||
import "@/css/obsidian-browser.css"
|
import "@/css/obsidian-browser.css"
|
||||||
|
import "@/css/io-chip.css"
|
||||||
|
|
||||||
const collections: Collection[] = [GraphLayoutCollection]
|
const collections: Collection[] = [GraphLayoutCollection]
|
||||||
import { useAuth } from "../context/AuthContext"
|
import { useAuth } from "../context/AuthContext"
|
||||||
|
|
@ -87,6 +90,7 @@ const customShapeUtils = [
|
||||||
ImageGenShape,
|
ImageGenShape,
|
||||||
VideoGenShape,
|
VideoGenShape,
|
||||||
MultmuxShape,
|
MultmuxShape,
|
||||||
|
IOChipShape,
|
||||||
]
|
]
|
||||||
const customTools = [
|
const customTools = [
|
||||||
ChatBoxTool,
|
ChatBoxTool,
|
||||||
|
|
@ -104,6 +108,7 @@ const customTools = [
|
||||||
ImageGenTool,
|
ImageGenTool,
|
||||||
VideoGenTool,
|
VideoGenTool,
|
||||||
MultmuxTool,
|
MultmuxTool,
|
||||||
|
IOChipTool,
|
||||||
]
|
]
|
||||||
|
|
||||||
// Debug: Log tool and shape registration info
|
// Debug: Log tool and shape registration info
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { BaseBoxShapeTool, TLEventHandlers } from 'tldraw'
|
||||||
|
|
||||||
|
export class IOChipTool extends BaseBoxShapeTool {
|
||||||
|
static override id = 'IOChip'
|
||||||
|
static override initial = 'idle'
|
||||||
|
override shapeType = 'IOChip'
|
||||||
|
|
||||||
|
override onComplete: TLEventHandlers["onComplete"] = () => {
|
||||||
|
console.log('🔌 IOChipTool: IO Chip created')
|
||||||
|
// Switch back to select tool after creating the chip
|
||||||
|
this.editor.setCurrentTool('select')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ import type { ObsidianObsNote } from "../lib/obsidianImporter"
|
||||||
import { HolonData } from "../lib/HoloSphereService"
|
import { HolonData } from "../lib/HoloSphereService"
|
||||||
import { FathomMeetingsPanel } from "../components/FathomMeetingsPanel"
|
import { FathomMeetingsPanel } from "../components/FathomMeetingsPanel"
|
||||||
import { getFathomApiKey, saveFathomApiKey, removeFathomApiKey, isFathomApiKeyConfigured } from "../lib/fathomApiKey"
|
import { getFathomApiKey, saveFathomApiKey, removeFathomApiKey, isFathomApiKeyConfigured } from "../lib/fathomApiKey"
|
||||||
|
import { ChipTemplateBrowser } from "../components/ChipTemplateBrowser"
|
||||||
|
|
||||||
// Dark mode utilities
|
// Dark mode utilities
|
||||||
const getDarkMode = (): boolean => {
|
const getDarkMode = (): boolean => {
|
||||||
|
|
@ -49,6 +50,21 @@ export function CustomToolbar() {
|
||||||
const [hasFathomApiKey, setHasFathomApiKey] = useState(false)
|
const [hasFathomApiKey, setHasFathomApiKey] = useState(false)
|
||||||
const profilePopupRef = useRef<HTMLDivElement>(null)
|
const profilePopupRef = useRef<HTMLDivElement>(null)
|
||||||
const [isDarkMode, setIsDarkMode] = useState(getDarkMode())
|
const [isDarkMode, setIsDarkMode] = useState(getDarkMode())
|
||||||
|
const [showChipTemplates, setShowChipTemplates] = useState(false)
|
||||||
|
|
||||||
|
// Listen for open-io-chip-templates event
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOpenChipTemplates = () => {
|
||||||
|
console.log('🔌 Opening IO Chip Template Browser')
|
||||||
|
setShowChipTemplates(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('open-io-chip-templates', handleOpenChipTemplates)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('open-io-chip-templates', handleOpenChipTemplates)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Initialize dark mode on mount
|
// Initialize dark mode on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1134,6 +1150,14 @@ export function CustomToolbar() {
|
||||||
isSelected={tools["Multmux"].id === editor.getCurrentToolId()}
|
isSelected={tools["Multmux"].id === editor.getCurrentToolId()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{tools["IOChip"] && (
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
{...tools["IOChip"]}
|
||||||
|
icon="rectangle"
|
||||||
|
label="IO Chip"
|
||||||
|
isSelected={tools["IOChip"].id === editor.getCurrentToolId()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{/* Share Location tool removed for now */}
|
{/* Share Location tool removed for now */}
|
||||||
{/* Refresh All ObsNotes Button */}
|
{/* Refresh All ObsNotes Button */}
|
||||||
{(() => {
|
{(() => {
|
||||||
|
|
@ -1159,7 +1183,15 @@ export function CustomToolbar() {
|
||||||
onClose={() => setShowFathomPanel(false)}
|
onClose={() => setShowFathomPanel(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* IO Chip Template Browser */}
|
||||||
|
{showChipTemplates && (
|
||||||
|
<ChipTemplateBrowser
|
||||||
|
editor={editor}
|
||||||
|
onClose={() => setShowChipTemplates(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,15 @@ export const overrides: TLUiOverrides = {
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect: () => editor.setCurrentTool("Multmux"),
|
onSelect: () => editor.setCurrentTool("Multmux"),
|
||||||
},
|
},
|
||||||
|
IOChip: {
|
||||||
|
id: "IOChip",
|
||||||
|
icon: "rectangle",
|
||||||
|
label: "IO Chip",
|
||||||
|
kbd: "alt+shift+p",
|
||||||
|
readonlyOk: true,
|
||||||
|
type: "IOChip",
|
||||||
|
onSelect: () => editor.setCurrentTool("IOChip"),
|
||||||
|
},
|
||||||
hand: {
|
hand: {
|
||||||
...tools.hand,
|
...tools.hand,
|
||||||
onDoubleClick: (info: any) => {
|
onDoubleClick: (info: any) => {
|
||||||
|
|
@ -335,6 +344,17 @@ export const overrides: TLUiOverrides = {
|
||||||
},
|
},
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
},
|
},
|
||||||
|
openIOChipTemplates: {
|
||||||
|
id: "open-io-chip-templates",
|
||||||
|
label: "IO Chip Templates",
|
||||||
|
kbd: "alt+t",
|
||||||
|
onSelect: () => {
|
||||||
|
// Dispatch event to open IO Chip Template Browser
|
||||||
|
const event = new CustomEvent('open-io-chip-templates')
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
},
|
||||||
|
readonlyOk: true,
|
||||||
|
},
|
||||||
moveSelectedLeft: {
|
moveSelectedLeft: {
|
||||||
id: "move-selected-left",
|
id: "move-selected-left",
|
||||||
label: "Move Left",
|
label: "Move Left",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue