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:
Jeff Emmett 2025-11-27 22:18:25 -08:00
parent a8c3988e3f
commit 527462acb7
9 changed files with 2105 additions and 1 deletions

View File

@ -127,6 +127,7 @@ import { FathomMeetingsBrowserShape } from "@/shapes/FathomMeetingsBrowserShapeU
import { ImageGenShape } from "@/shapes/ImageGenShapeUtil"
import { VideoGenShape } from "@/shapes/VideoGenShapeUtil"
import { MultmuxShape } from "@/shapes/MultmuxShapeUtil"
import { IOChipShape } from "@/shapes/IOChipShapeUtil"
// Location shape removed - no longer needed
export function useAutomergeStoreV2({
@ -160,6 +161,7 @@ export function useAutomergeStoreV2({
ImageGen: {} as any,
VideoGen: {} as any,
Multmux: {} as any,
IOChip: {} as any,
},
bindings: defaultBindingSchemas,
})
@ -184,6 +186,7 @@ export function useAutomergeStoreV2({
ImageGenShape,
VideoGenShape,
MultmuxShape,
IOChipShape,
],
})
return store

View File

@ -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 }
}

153
src/css/io-chip.css Normal file
View File

@ -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;
}

View File

@ -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()

View File

@ -46,6 +46,8 @@ import { VideoGenShape } from "@/shapes/VideoGenShapeUtil"
import { VideoGenTool } from "@/tools/VideoGenTool"
import { MultmuxTool } from "@/tools/MultmuxTool"
import { MultmuxShape } from "@/shapes/MultmuxShapeUtil"
import { IOChipTool } from "@/tools/IOChipTool"
import { IOChipShape } from "@/shapes/IOChipShapeUtil"
import {
lockElement,
unlockElement,
@ -62,6 +64,7 @@ import { CmdK } from "@/CmdK"
import "react-cmdk/dist/cmdk.css"
import "@/css/style.css"
import "@/css/obsidian-browser.css"
import "@/css/io-chip.css"
const collections: Collection[] = [GraphLayoutCollection]
import { useAuth } from "../context/AuthContext"
@ -87,6 +90,7 @@ const customShapeUtils = [
ImageGenShape,
VideoGenShape,
MultmuxShape,
IOChipShape,
]
const customTools = [
ChatBoxTool,
@ -104,6 +108,7 @@ const customTools = [
ImageGenTool,
VideoGenTool,
MultmuxTool,
IOChipTool,
]
// Debug: Log tool and shape registration info

File diff suppressed because it is too large Load Diff

13
src/tools/IOChipTool.ts Normal file
View File

@ -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')
}
}

View File

@ -16,6 +16,7 @@ import type { ObsidianObsNote } from "../lib/obsidianImporter"
import { HolonData } from "../lib/HoloSphereService"
import { FathomMeetingsPanel } from "../components/FathomMeetingsPanel"
import { getFathomApiKey, saveFathomApiKey, removeFathomApiKey, isFathomApiKeyConfigured } from "../lib/fathomApiKey"
import { ChipTemplateBrowser } from "../components/ChipTemplateBrowser"
// Dark mode utilities
const getDarkMode = (): boolean => {
@ -49,6 +50,21 @@ export function CustomToolbar() {
const [hasFathomApiKey, setHasFathomApiKey] = useState(false)
const profilePopupRef = useRef<HTMLDivElement>(null)
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
useEffect(() => {
@ -1134,6 +1150,14 @@ export function CustomToolbar() {
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 */}
{/* Refresh All ObsNotes Button */}
{(() => {
@ -1159,7 +1183,15 @@ export function CustomToolbar() {
onClose={() => setShowFathomPanel(false)}
/>
)}
{/* IO Chip Template Browser */}
{showChipTemplates && (
<ChipTemplateBrowser
editor={editor}
onClose={() => setShowChipTemplates(false)}
/>
)}
</div>
)
}

View File

@ -228,6 +228,15 @@ export const overrides: TLUiOverrides = {
readonlyOk: true,
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: {
...tools.hand,
onDoubleClick: (info: any) => {
@ -335,6 +344,17 @@ export const overrides: TLUiOverrides = {
},
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: {
id: "move-selected-left",
label: "Move Left",