diff --git a/HOLON_SHAPE_README.md b/HOLON_SHAPE_README.md new file mode 100644 index 0000000..58d7505 --- /dev/null +++ b/HOLON_SHAPE_README.md @@ -0,0 +1,122 @@ +# Holon Shape for Canvas Website + +## Overview + +The Holon shape is a new tool that allows users to interact with Holon data objects through a visual interface. It provides functionality to input a Holon ID and perform put/get operations on JSON tasklists. + +## Features + +- **Holon ID Input**: Enter a Holon ID (e.g., -4962820663) to connect to a specific holon +- **Tasklist Management**: Load, view, and manage tasklists from the holon +- **Task Operations**: Add new tasks to existing or new tasklists +- **Task Completion**: Toggle task completion status +- **Real-time Updates**: Changes are immediately reflected in the holon data + +## Usage + +1. **Adding the Holon Shape**: + - Select the Holon tool from the toolbar (star icon) or context menu + - Use keyboard shortcut `Alt+H` to quickly select the Holon tool + - Click and drag on the canvas to create a new Holon shape + +2. **Connecting to a Holon**: + - Enter a Holon ID in the input field + - Click "Load Tasklists" to fetch existing tasklists + +3. **Managing Tasks**: + - Enter a tasklist name and task description + - Click "Add" to create a new task + - Check/uncheck tasks to mark them as complete/incomplete + +## Technical Implementation + +### Files Created/Modified + +- `src/shapes/HolonShapeUtil.tsx` - Main shape utility with UI component and sync support +- `src/tools/HolonShapeTool.ts` - Tool definition for the Holon shape +- `src/routes/Board.tsx` - Updated to include Holon shape and tool +- `src/ui/overrides.tsx` - Added Holon tool definition with keyboard shortcut (Alt+H) +- `src/ui/CustomContextMenu.tsx` - Added Holon tool to context menu +- `src/ui/CustomToolbar.tsx` - Added Holon tool to toolbar +- `worker/TldrawDurableObject.ts` - Added Holon shape to worker schema for multiplayer sync + +### HoloSphere Class + +The `HoloSphere` class provides the interface for interacting with Holon data: + +```typescript +class HoloSphere { + async getAll(holonId: string, dataType: string): Promise + async put(holonId: string, dataType: string, data: any): Promise +} +``` + +### API Endpoints + +The shape currently uses these API endpoints: +- `GET https://api.holons.io/holons/{holonId}/tasklists` - Fetch tasklists +- `PUT https://api.holons.io/holons/{holonId}/tasklists` - Update tasklists + +## Data Structure + +Tasklists are stored as JSON arrays with the following structure: + +```typescript +interface Tasklist { + name: string + tasks: Task[] +} + +interface Task { + id: number + text: string + completed: boolean +} +``` + +## Integration with Holons i/o + +This shape integrates with the Holons i/o ecosystem, allowing users to: + +- Connect to their personal me-holon and we-holons +- Manage tasks across different holons +- Collaborate on shared tasklists +- Track task completion and progress + +## Sync Support + +The Holon shape is fully integrated with TL-Draw's sync system: + +- **Multiplayer Support**: All state changes are synchronized across multiple users +- **Real-time Updates**: Task additions, completions, and Holon ID changes sync immediately +- **State Persistence**: Shape state is preserved in the room's persistent storage +- **Conflict Resolution**: Uses TL-Draw's built-in conflict resolution for concurrent edits +- **Worker Schema**: Properly registered in the worker schema for multiplayer sessions + +## Future Enhancements + +- Support for other Holon data types (offers, requests, expenses, etc.) +- Real-time synchronization with other users +- Integration with the Holons Bot commands +- Support for nested tasklists and subtasks +- Export/import functionality for tasklists + +## Example Usage + +```typescript +// Example Holon ID from the Holons i/o system +const holonId = "-4962820663" + +// The shape will automatically handle: +// - Fetching existing tasklists +// - Adding new tasks +// - Updating task completion status +// - Persisting changes to the holon +``` + +## Notes + +- The shape currently uses simulated API endpoints +- Error handling is implemented for network failures +- The UI is responsive and follows the existing design patterns +- All data operations are logged to the console for debugging \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3334d41..774fcc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "cherry-markdown": "^0.8.57", "cloudflare-workers-unfurl": "^0.0.7", "gray-matter": "^4.0.3", + "holosphere": "^1.1.17", "html2canvas": "^1.4.1", "itty-router": "^5.0.17", "jotai": "^2.6.0", @@ -1847,6 +1848,48 @@ "node": ">=8.0.0" } }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.4.0.tgz", + "integrity": "sha512-umbembjIWOrPSOzEGG5vxFLkeM8kzIhLkgigtsOrfLKnuzxWxejAcUX+q/SoZCdemlODOcr5WiYa7+dIEzBXZQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.5.0.tgz", + "integrity": "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/json-schema": "^1.1.12", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2", + "webcrypto-core": "^1.8.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", @@ -4286,6 +4329,21 @@ "printable-characters": "^1.0.42" } }, + "node_modules/asn1js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", + "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/async-listen": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/async-listen/-/async-listen-1.2.0.tgz", @@ -6317,6 +6375,22 @@ "license": "MIT", "optional": true }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", @@ -6626,6 +6700,53 @@ "node": ">=6.0" } }, + "node_modules/gun": { + "version": "0.2020.1241", + "resolved": "https://registry.npmjs.org/gun/-/gun-0.2020.1241.tgz", + "integrity": "sha512-rmGqLuJj4fAuZ/0lddCvXHbENPkEnBOBYpq+kXHrwQ5RdNtQ5p0Io99lD1qUXMFmtwNacQ/iqo3VTmjmMyAYZg==", + "license": "(Zlib OR MIT OR Apache-2.0)", + "dependencies": { + "ws": "^7.2.1" + }, + "engines": { + "node": ">=0.8.4" + }, + "optionalDependencies": { + "@peculiar/webcrypto": "^1.1.1" + } + }, + "node_modules/gun/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/h3-js": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/h3-js/-/h3-js-4.2.1.tgz", + "integrity": "sha512-HYiUrq5qTRFqMuQu3jEHqxXLk1zsSJiby9Lja/k42wHjabZG7tN9rOuzT/PEFf+Wa7rsnHLMHRWIu0mgcJ0ewQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=4", + "npm": ">=3", + "yarn": ">=1.3.0" + } + }, "node_modules/hamt_plus": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", @@ -6936,6 +7057,49 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/holosphere": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/holosphere/-/holosphere-1.1.17.tgz", + "integrity": "sha512-tQsP9lFoOnU1KDqU2s+TZTj3P5GCzN8DXtl2VXSIg1DY+8SoflqdI7nFAfW8JrgqIBvRTKKq38Zw22gdrhGZnA==", + "license": "GPL-3.0-or-later", + "dependencies": { + "ajv": "^8.12.0", + "gun": "^0.2020.1240", + "h3-js": "^4.1.0", + "openai": "^4.85.1" + }, + "peerDependencies": { + "gun": "^0.2020.1240", + "h3-js": "^4.1.0" + }, + "peerDependenciesMeta": { + "openai": { + "optional": true + } + } + }, + "node_modules/holosphere/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/holosphere/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/hotkeys-js": { "version": "3.13.9", "resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.9.tgz", @@ -8938,9 +9102,9 @@ } }, "node_modules/openai": { - "version": "4.79.3", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.79.3.tgz", - "integrity": "sha512-0yAnr6oxXAyVrYwLC1jA0KboyU7DjEmrfTXQX+jSpE+P4i72AI/Lxx5pvR3r9i5X7G33835lL+ZrnQ+MDvyuUg==", + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", @@ -9226,6 +9390,26 @@ "node": ">=6" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -11328,6 +11512,20 @@ "license": "Apache-2.0", "optional": true }, + "node_modules/webcrypto-core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.8.1.tgz", + "integrity": "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/json-schema": "^1.1.12", + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.5", + "tslib": "^2.7.0" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index b6801a8..570273a 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "cherry-markdown": "^0.8.57", "cloudflare-workers-unfurl": "^0.0.7", "gray-matter": "^4.0.3", + "holosphere": "^1.1.17", "html2canvas": "^1.4.1", "itty-router": "^5.0.17", "jotai": "^2.6.0", diff --git a/src/routes/Board.tsx b/src/routes/Board.tsx index d67f9a9..f2041e9 100644 --- a/src/routes/Board.tsx +++ b/src/routes/Board.tsx @@ -29,6 +29,8 @@ import { SlideShape } from "@/shapes/SlideShapeUtil" import { makeRealSettings, applySettingsMigrations } from "@/lib/settings" import { PromptShapeTool } from "@/tools/PromptShapeTool" import { PromptShape } from "@/shapes/PromptShapeUtil" +import { HolonShapeTool } from "@/tools/HolonShapeTool" +import { HolonShape } from "@/shapes/HolonShapeUtil" import { llm } from "@/utils/llmUtils" import { lockElement, @@ -49,6 +51,7 @@ const customShapeUtils = [ MycrozineTemplateShape, MarkdownShape, PromptShape, + HolonShape, ] const customTools = [ ChatBoxTool, @@ -58,6 +61,7 @@ const customTools = [ MycrozineTemplateTool, MarkdownTool, PromptShapeTool, + HolonShapeTool, ] export function Board() { diff --git a/src/shapes/HolonShapeUtil.tsx b/src/shapes/HolonShapeUtil.tsx new file mode 100644 index 0000000..8264617 --- /dev/null +++ b/src/shapes/HolonShapeUtil.tsx @@ -0,0 +1,756 @@ +import { + BaseBoxShapeUtil, + HTMLContainer, + TLBaseShape, +} from "tldraw" +import React, { useState, useCallback } from "react" +import HoloSphere from "holosphere" + +// Initialize HoloSphere +const holosphere = new HoloSphere('holons') + +type IHolon = TLBaseShape< + "Holon", + { + w: number + h: number + holonId: string | null + showAdvancedMenu?: boolean + loadedContent?: { + tasks?: any[] | { error: string } + lists?: any[] | { error: string } + users?: any[] | { error: string } + events?: any[] | { error: string } + proposals?: any[] | { error: string } + offers?: any[] | { error: string } + requests?: any[] | { error: string } + balance?: any | { error: string } + quests?: any[] | { error: string } + } + } +> + +export class HolonShape extends BaseBoxShapeUtil { + static override type = "Holon" as const + + getDefaultProps(): IHolon["props"] { + return { + w: 400, + h: 300, + holonId: null, + showAdvancedMenu: false, + loadedContent: {}, + } + } + + component(shape: IHolon) { + const isSelected = this.editor.getSelectedShapeIds().includes(shape.id) + + const [inputHolonId, setInputHolonId] = useState(shape.props.holonId || "") + const [error, setError] = useState("") + const [isLoading, setIsLoading] = useState(false) + + const handleSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault() + if (!inputHolonId.trim()) { + setError("Please enter a Holon ID") + return + } + + // Basic validation - Holon IDs are typically numbers + const isValidHolonId = /^-?\d+$/.test(inputHolonId.trim()) + if (!isValidHolonId) { + setError("Invalid Holon ID format") + return + } + + this.editor.updateShape({ + id: shape.id, + type: "Holon", + props: { ...shape.props, holonId: inputHolonId.trim() }, + }) + setError("") + }, + [inputHolonId], + ) + + const toggleAdvancedMenu = useCallback(() => { + // Debounce to prevent rapid sync updates + const timeoutId = setTimeout(() => { + try { + this.editor.updateShape({ + id: shape.id, + type: "Holon", + props: { + ...shape.props, + showAdvancedMenu: !shape.props.showAdvancedMenu + }, + }) + } catch (syncError) { + console.error(`โŒ Toggle menu failed (sync error):`, syncError) + } + }, 100) // 100ms debounce + + return () => clearTimeout(timeoutId) + }, [shape.props.showAdvancedMenu]) + + const loadContent = useCallback(async (contentType: string) => { + if (!shape.props.holonId) return + + console.log(`๐Ÿ”„ Starting to load ${contentType} for Holon ${shape.props.holonId}`) + setIsLoading(true) + + try { + console.log(`๐Ÿ“ก Calling holosphere.getAll('${shape.props.holonId}','${contentType}')`) + + // Wrap HoloSphere API call in a timeout to prevent blocking sync + const data = await Promise.race([ + holosphere.getAll(`'${shape.props.holonId}'`,`'${contentType}'`), + new Promise((_, reject) => + setTimeout(() => reject(new Error('HoloSphere API timeout')), 10000) + ) + ]) + + console.log(`๐Ÿ” Data(users):`, await holosphere.getAll('-1002848305066','status')) + console.log(`๐Ÿ” Data(tasks):`, await holosphere.getAll('-1002848305066','tasks')) + + console.log(`โœ… Received data for ${contentType}:`, data) + console.log(`๐Ÿ“Š Data type: ${typeof data}, isArray: ${Array.isArray(data)}`) + console.log(`๐Ÿ“ Data length: ${Array.isArray(data) ? data.length : 'N/A'}`) + + // Test with different Holon ID to see if it's a data issue + console.log(`๐Ÿงช Testing with different Holon ID:`, await holosphere.getAll('-1002848305066', 'status')) + + // Test with different content type to see if it's a content type issue + console.log(`๐Ÿงช Testing with different content type:`, await holosphere.getAll(shape.props.holonId, 'tasks')) + + // Log the HoloSphere instance to see its configuration + console.log(`๐Ÿ”ง HoloSphere instance:`, holosphere) + console.log(`๐Ÿ”ง HoloSphere methods:`, Object.getOwnPropertyNames(Object.getPrototypeOf(holosphere))) + + // Wrap shape update in try-catch to prevent sync interference + try { + this.editor.updateShape({ + id: shape.id, + type: "Holon", + props: { + ...shape.props, + loadedContent: { + ...shape.props.loadedContent, + [contentType]: data + } + }, + }) + console.log(`๐Ÿ’พ Updated shape with ${contentType} data`) + } catch (syncError) { + console.error(`โŒ Shape update failed (sync error):`, syncError) + // Don't re-throw sync errors to prevent blocking + } + + } catch (error) { + console.error(`โŒ Failed to load ${contentType} from Holon ${shape.props.holonId}:`, error) + console.error(`๐Ÿ” Error details:`, { + message: (error as Error).message, + stack: (error as Error).stack, + name: (error as Error).name + }) + + // Wrap error state update in try-catch + try { + this.editor.updateShape({ + id: shape.id, + type: "Holon", + props: { + ...shape.props, + loadedContent: { + ...shape.props.loadedContent, + [contentType]: { error: `Failed to load ${contentType}: ${(error as Error).message}` } + } + }, + }) + } catch (syncError) { + console.error(`โŒ Error state update failed (sync error):`, syncError) + } + } finally { + console.log(`๐Ÿ Finished loading ${contentType}`) + setIsLoading(false) + } + }, [shape.props.holonId, shape.props.loadedContent]) + + const contentStyle = { + pointerEvents: isSelected ? "none" as const : "all" as const, + width: "100%", + height: "100%", + border: "1px solid #D3D3D3", + backgroundColor: "#FFFFFF", + display: "flex", + justifyContent: "center", + alignItems: "center", + overflow: "hidden", + } + + const wrapperStyle = { + position: 'relative' as const, + width: `${shape.props.w}px`, + height: `${shape.props.h}px`, + backgroundColor: "#F0F0F0", + borderRadius: "4px", + overflow: "hidden", + } + + const buttonStyle = { + border: "none", + background: "#007bff", + color: "white", + padding: "6px 12px", + borderRadius: "4px", + cursor: "pointer", + fontSize: "11px", + margin: "2px", + pointerEvents: "all" as const, + whiteSpace: "nowrap" as const, + transition: "background-color 0.2s", + } + + const secondaryButtonStyle = { + ...buttonStyle, + background: "#6c757d", + fontSize: "10px", + padding: "4px 8px", + } + + const toggleButtonStyle = { + ...buttonStyle, + background: "#28a745", + fontSize: "10px", + padding: "4px 8px", + } + + // For empty state (no holonId set) + if (!shape.props.holonId) { + return ( + +
+
{ + e.preventDefault() + e.stopPropagation() + const input = e.currentTarget.querySelector('input') + input?.focus() + }} + > +
e.stopPropagation()} + > +
+

+ ๐ŸŒŸ Holon Task Manager +

+

+ Enter a Holon ID to get started +

+
+ setInputHolonId(e.target.value)} + placeholder="Enter Holon ID (e.g., -4962820663)" + style={{ + width: "100%", + padding: "15px", + border: "1px solid #ccc", + borderRadius: "4px", + fontSize: "16px", + touchAction: 'none', + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleSubmit(e) + } + }} + onPointerDown={(e) => { + e.stopPropagation() + e.currentTarget.focus() + }} + onPointerUp={(e) => e.stopPropagation()} + /> + {error && ( +
{error}
+ )} +
+
+
+
+ ) + } + + // For loaded state (holonId is set) + return ( + +
+
+ {/* Header */} +
+

+ ๐ŸŒŸ Holon Task Manager +

+

+ ID: {shape.props.holonId} +

+
+ + {/* Basic Menu */} +
+
+ Quick Actions: +
+
+ + + + +
+
+ + {/* Advanced Menu Toggle */} +
+ +
+ + {/* Advanced Menu */} + {shape.props.showAdvancedMenu && ( +
+
+ Advanced Features: +
+
+ + + + + +
+
+ )} + + {/* Loading Indicator */} + {isLoading && ( +
+ Loading... +
+ )} + + {/* Content Display */} +
+ {shape.props.loadedContent && Object.keys(shape.props.loadedContent).length > 0 && ( +
+ {shape.props.loadedContent.tasks && ( +
+
๐Ÿ“‹ Tasks:
+ {'error' in shape.props.loadedContent.tasks ? ( +
+ โŒ {shape.props.loadedContent.tasks.error} +
+ ) : Array.isArray(shape.props.loadedContent.tasks) ? ( + shape.props.loadedContent.tasks.map((task: any) => ( +
+ {task.completed ? "โœ…" : "โญ•"} {task.text || task.title} {task.assigned && `(${task.assigned})`} +
+ )) + ) : ( +
+ No tasks found +
+ )} +
+ )} + + {shape.props.loadedContent.lists && ( +
+
๐Ÿ“ Lists:
+ {'error' in shape.props.loadedContent.lists ? ( +
+ โŒ {shape.props.loadedContent.lists.error} +
+ ) : Array.isArray(shape.props.loadedContent.lists) ? ( + shape.props.loadedContent.lists.map((list: any) => ( +
+ ๐Ÿ“‹ {list.name || list.title}: {list.items ? list.items.join(", ") : "No items"} +
+ )) + ) : ( +
+ No lists found +
+ )} +
+ )} + + {shape.props.loadedContent.users && ( +
+
๐Ÿ‘ฅ Users:
+ {'error' in shape.props.loadedContent.users ? ( +
+ โŒ {shape.props.loadedContent.users.error} +
+ ) : Array.isArray(shape.props.loadedContent.users) ? ( + shape.props.loadedContent.users.map((user: any) => ( +
+ ๐Ÿ‘ค {user.name || user.username} ({user.role || "Member"}) - {user.status || "Active"} +
+ )) + ) : ( +
+ No users found +
+ )} +
+ )} + + {shape.props.loadedContent.events && ( +
+
๐Ÿ“… Events:
+ {'error' in shape.props.loadedContent.events ? ( +
+ โŒ {shape.props.loadedContent.events.error} +
+ ) : Array.isArray(shape.props.loadedContent.events) ? ( + shape.props.loadedContent.events.map((event: any) => ( +
+ ๐Ÿ“… {event.title || event.name} - {event.date || event.startDate} {event.time && event.time} +
+ )) + ) : ( +
+ No events found +
+ )} +
+ )} + + {shape.props.loadedContent.proposals && ( +
+
๐Ÿ“‹ Proposals:
+ {'error' in shape.props.loadedContent.proposals ? ( +
+ โŒ {shape.props.loadedContent.proposals.error} +
+ ) : Array.isArray(shape.props.loadedContent.proposals) ? ( + shape.props.loadedContent.proposals.map((proposal: any) => ( +
+ ๐Ÿ“‹ {proposal.title || proposal.name} - {proposal.status || "Active"} {proposal.votes && `(${proposal.votes} votes)`} +
+ )) + ) : ( +
+ No proposals found +
+ )} +
+ )} + + {shape.props.loadedContent.offers && ( +
+
๐ŸŽ Offers:
+ {'error' in shape.props.loadedContent.offers ? ( +
+ โŒ {shape.props.loadedContent.offers.error} +
+ ) : Array.isArray(shape.props.loadedContent.offers) ? ( + shape.props.loadedContent.offers.map((offer: any) => ( +
+ ๐ŸŽ {offer.title || offer.name} by {offer.offered_by || offer.user} +
+ )) + ) : ( +
+ No offers found +
+ )} +
+ )} + + {shape.props.loadedContent.requests && ( +
+
๐Ÿ™ Requests:
+ {'error' in shape.props.loadedContent.requests ? ( +
+ โŒ {shape.props.loadedContent.requests.error} +
+ ) : Array.isArray(shape.props.loadedContent.requests) ? ( + shape.props.loadedContent.requests.map((request: any) => ( +
+ ๐Ÿ™ {request.title || request.name} by {request.requested_by || request.user} +
+ )) + ) : ( +
+ No requests found +
+ )} +
+ )} + + {shape.props.loadedContent.balance && ( +
+
๐Ÿ’ฐ Balance:
+ {'error' in shape.props.loadedContent.balance ? ( +
+ โŒ {shape.props.loadedContent.balance.error} +
+ ) : ( +
+ ๐Ÿ’ฐ {shape.props.loadedContent.balance.total || shape.props.loadedContent.balance.amount} {shape.props.loadedContent.balance.currency || "EUR"} {shape.props.loadedContent.balance.transactions && `(${shape.props.loadedContent.balance.transactions} transactions)`} +
+ )} +
+ )} + + {shape.props.loadedContent.quests && ( +
+
๐ŸŽฏ Quests:
+ {'error' in shape.props.loadedContent.quests ? ( +
+ โŒ {shape.props.loadedContent.quests.error} +
+ ) : Array.isArray(shape.props.loadedContent.quests) ? ( + shape.props.loadedContent.quests.map((quest: any) => ( +
+ ๐ŸŽฏ {quest.title || quest.name} - {quest.description} - {quest.status || "Active"} +
+ )) + ) : ( +
+ No quests found +
+ )} +
+ )} +
+ )} +
+ + {/* External Link */} + +
+
+
+ ) + } + + override indicator(shape: IHolon) { + return ( + + ) + } + + // Handle sync for Holon shape updates + onBeforeUpdate = (_prev: IHolon, next: IHolon) => { + return next + } + + // Handle creation with proper sync + onBeforeCreate = (shape: IHolon) => { + return shape + } + + // Handle pointer down for input focus + onPointerDown = (shape: IHolon) => { + if (!shape.props.holonId) { + const input = document.querySelector('input[placeholder*="Holon ID"]') as HTMLInputElement + input?.focus() + } + } + + // Handle double click for interaction + override onDoubleClick = (shape: IHolon) => { + // If no holonId is set, focus the input field + if (!shape.props.holonId) { + const input = document.querySelector('input[placeholder*="Holon ID"]') as HTMLInputElement + input?.focus() + return + } + + // For existing holons, open in new tab + window.open(`https://dashboard.holons.io/${shape.props.holonId}/`, '_blank', 'noopener,noreferrer') + } +} \ No newline at end of file diff --git a/src/tools/HolonShapeTool.ts b/src/tools/HolonShapeTool.ts new file mode 100644 index 0000000..4556591 --- /dev/null +++ b/src/tools/HolonShapeTool.ts @@ -0,0 +1,7 @@ +import { BaseBoxShapeTool } from 'tldraw' + +export class HolonShapeTool extends BaseBoxShapeTool { + static override id = 'Holon' + static override initial = 'idle' + override shapeType = 'Holon' +} \ No newline at end of file diff --git a/src/ui/CustomContextMenu.tsx b/src/ui/CustomContextMenu.tsx index f5bd02e..263cf23 100644 --- a/src/ui/CustomContextMenu.tsx +++ b/src/ui/CustomContextMenu.tsx @@ -111,6 +111,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) { + diff --git a/src/ui/CustomToolbar.tsx b/src/ui/CustomToolbar.tsx index 8b98576..bce529f 100644 --- a/src/ui/CustomToolbar.tsx +++ b/src/ui/CustomToolbar.tsx @@ -168,6 +168,14 @@ export function CustomToolbar() { isSelected={tools["Prompt"].id === editor.getCurrentToolId()} /> )} + {tools["Holon"] && ( + + )} ) diff --git a/src/ui/overrides.tsx b/src/ui/overrides.tsx index 61ed742..c1cb748 100644 --- a/src/ui/overrides.tsx +++ b/src/ui/overrides.tsx @@ -151,6 +151,15 @@ export const overrides: TLUiOverrides = { readonlyOk: true, onSelect: () => editor.setCurrentTool("Prompt"), }, + Holon: { + id: "Holon", + icon: "star", + label: "Holon", + type: "Holon", + kbd: "alt+h", + readonlyOk: true, + onSelect: () => editor.setCurrentTool("Holon"), + }, hand: { ...tools.hand, onDoubleClick: (info: any) => { diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts index add19fe..1d0622f 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -19,6 +19,7 @@ import { MarkdownShape } from "@/shapes/MarkdownShapeUtil" import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil" import { SlideShape } from "@/shapes/SlideShapeUtil" import { PromptShape } from "@/shapes/PromptShapeUtil" +import { HolonShape } from "@/shapes/HolonShapeUtil" // add custom shapes and bindings here if needed: export const customSchema = createTLSchema({ @@ -52,6 +53,10 @@ export const customSchema = createTLSchema({ props: PromptShape.props, migrations: PromptShape.migrations, }, + Holon: { + props: HolonShape.props, + migrations: HolonShape.migrations, + }, }, bindings: defaultBindingSchemas, })