@tangle-network/ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
- package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
- package/dist/auth.d.ts +74 -0
- package/dist/auth.js +15 -0
- package/dist/button-CMQuQEW_.d.ts +17 -0
- package/dist/chat.d.ts +232 -0
- package/dist/chat.js +30 -0
- package/dist/chunk-2NFQRQOD.js +1009 -0
- package/dist/chunk-2VH6PUXD.js +186 -0
- package/dist/chunk-34A66VBG.js +214 -0
- package/dist/chunk-3OI2QKFD.js +0 -0
- package/dist/chunk-4CLN43XT.js +45 -0
- package/dist/chunk-54SQQMMM.js +156 -0
- package/dist/chunk-5Z5ZYMOJ.js +0 -0
- package/dist/chunk-66BNMOVT.js +167 -0
- package/dist/chunk-6BGQA4BQ.js +0 -0
- package/dist/chunk-7UO2ZMRQ.js +133 -0
- package/dist/chunk-BX6AQMUS.js +183 -0
- package/dist/chunk-CD53GZOM.js +59 -0
- package/dist/chunk-CSAIKY36.js +54 -0
- package/dist/chunk-EEE55AVS.js +1201 -0
- package/dist/chunk-GYPQXTJU.js +230 -0
- package/dist/chunk-HFL6R6IF.js +37 -0
- package/dist/chunk-HJKCSXCH.js +737 -0
- package/dist/chunk-LISXUB4D.js +1222 -0
- package/dist/chunk-LQS34IGP.js +0 -0
- package/dist/chunk-MKTSMWVD.js +109 -0
- package/dist/chunk-NKDZ7GZE.js +192 -0
- package/dist/chunk-OEX7NZE3.js +321 -0
- package/dist/chunk-Q56BYXQF.js +61 -0
- package/dist/chunk-Q7EIIWTC.js +0 -0
- package/dist/chunk-REJESC5U.js +117 -0
- package/dist/chunk-RQGKSCEZ.js +0 -0
- package/dist/chunk-RQHJBTEU.js +10 -0
- package/dist/chunk-TMFOPHHN.js +299 -0
- package/dist/chunk-XGKULLYE.js +40 -0
- package/dist/chunk-XIHMJ7ZQ.js +614 -0
- package/dist/chunk-YJ2G3XO5.js +1048 -0
- package/dist/chunk-YNN4O57I.js +754 -0
- package/dist/code-block-DjXf8eOG.d.ts +19 -0
- package/dist/document-editor-pane-A5LT5H4N.js +12 -0
- package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
- package/dist/editor.d.ts +120 -0
- package/dist/editor.js +34 -0
- package/dist/files.d.ts +175 -0
- package/dist/files.js +20 -0
- package/dist/hooks.d.ts +56 -0
- package/dist/hooks.js +41 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +446 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +14 -0
- package/dist/message-BHWbxBtT.d.ts +15 -0
- package/dist/openui.d.ts +115 -0
- package/dist/openui.js +12 -0
- package/dist/parts-dj7AcUg0.d.ts +36 -0
- package/dist/primitives.d.ts +332 -0
- package/dist/primitives.js +191 -0
- package/dist/run-PfLmDAox.d.ts +41 -0
- package/dist/run.d.ts +69 -0
- package/dist/run.js +36 -0
- package/dist/sdk-hooks.d.ts +285 -0
- package/dist/sdk-hooks.js +31 -0
- package/dist/stores.d.ts +17 -0
- package/dist/stores.js +76 -0
- package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
- package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
- package/dist/tool-previews.d.ts +48 -0
- package/dist/tool-previews.js +21 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +45 -0
- package/dist/utils.js +32 -0
- package/package.json +193 -0
- package/src/auth/auth.tsx +228 -0
- package/src/auth/index.ts +13 -0
- package/src/auth/login-layout.tsx +46 -0
- package/src/chat/agent-timeline.stories.tsx +429 -0
- package/src/chat/agent-timeline.tsx +360 -0
- package/src/chat/chat-container.tsx +486 -0
- package/src/chat/chat-input.stories.tsx +142 -0
- package/src/chat/chat-input.tsx +389 -0
- package/src/chat/chat-message.stories.tsx +237 -0
- package/src/chat/chat-message.tsx +129 -0
- package/src/chat/index.ts +18 -0
- package/src/chat/message-list.stories.tsx +336 -0
- package/src/chat/message-list.tsx +79 -0
- package/src/chat/thinking-indicator.stories.tsx +56 -0
- package/src/chat/thinking-indicator.tsx +30 -0
- package/src/chat/user-message.stories.tsx +92 -0
- package/src/chat/user-message.tsx +43 -0
- package/src/editor/document-editor-pane.tsx +351 -0
- package/src/editor/editor-provider.tsx +428 -0
- package/src/editor/editor-toolbar.tsx +130 -0
- package/src/editor/index.ts +31 -0
- package/src/editor/markdown-conversion.ts +21 -0
- package/src/editor/markdown-document-editor.tsx +137 -0
- package/src/editor/tiptap-editor.tsx +331 -0
- package/src/editor/use-editor.ts +221 -0
- package/src/files/file-artifact-pane.tsx +183 -0
- package/src/files/file-preview.tsx +342 -0
- package/src/files/file-tabs.tsx +71 -0
- package/src/files/file-tree.tsx +258 -0
- package/src/files/index.ts +17 -0
- package/src/files/rich-file-tree.stories.tsx +104 -0
- package/src/files/rich-file-tree.test.tsx +42 -0
- package/src/files/rich-file-tree.tsx +232 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-auth.ts +153 -0
- package/src/hooks/use-auto-scroll.ts +59 -0
- package/src/hooks/use-dropdown-menu.ts +40 -0
- package/src/hooks/use-live-time.test.tsx +40 -0
- package/src/hooks/use-live-time.ts +27 -0
- package/src/hooks/use-realtime-session.ts +319 -0
- package/src/hooks/use-run-collapse-state.ts +25 -0
- package/src/hooks/use-run-groups.ts +111 -0
- package/src/hooks/use-sdk-session.ts +575 -0
- package/src/hooks/use-sse-stream.ts +475 -0
- package/src/hooks/use-tool-call-stream.ts +96 -0
- package/src/index.ts +14 -0
- package/src/lib/utils.ts +6 -0
- package/src/markdown/code-block.tsx +198 -0
- package/src/markdown/index.ts +2 -0
- package/src/markdown/markdown.stories.tsx +190 -0
- package/src/markdown/markdown.tsx +62 -0
- package/src/openui/index.ts +20 -0
- package/src/openui/openui-artifact-renderer.tsx +542 -0
- package/src/primitives/artifact-pane.tsx +91 -0
- package/src/primitives/avatar.stories.tsx +95 -0
- package/src/primitives/avatar.tsx +47 -0
- package/src/primitives/badge.stories.tsx +57 -0
- package/src/primitives/badge.tsx +97 -0
- package/src/primitives/button.stories.tsx +48 -0
- package/src/primitives/button.tsx +115 -0
- package/src/primitives/card.stories.tsx +53 -0
- package/src/primitives/card.tsx +98 -0
- package/src/primitives/code-block.stories.tsx +115 -0
- package/src/primitives/code-block.tsx +22 -0
- package/src/primitives/design-tokens.stories.tsx +162 -0
- package/src/primitives/dialog.stories.tsx +176 -0
- package/src/primitives/dialog.tsx +137 -0
- package/src/primitives/drop-zone.stories.tsx +123 -0
- package/src/primitives/drop-zone.tsx +131 -0
- package/src/primitives/dropdown-menu.stories.tsx +122 -0
- package/src/primitives/dropdown-menu.tsx +214 -0
- package/src/primitives/empty-state.stories.tsx +81 -0
- package/src/primitives/empty-state.tsx +40 -0
- package/src/primitives/index.ts +118 -0
- package/src/primitives/input.stories.tsx +113 -0
- package/src/primitives/input.tsx +136 -0
- package/src/primitives/label.stories.tsx +84 -0
- package/src/primitives/label.tsx +24 -0
- package/src/primitives/progress.stories.tsx +93 -0
- package/src/primitives/progress.tsx +50 -0
- package/src/primitives/segmented-control.test.tsx +328 -0
- package/src/primitives/segmented-control.tsx +154 -0
- package/src/primitives/select.stories.tsx +164 -0
- package/src/primitives/select.tsx +158 -0
- package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
- package/src/primitives/sidebar-drop-zone.tsx +149 -0
- package/src/primitives/skeleton.stories.tsx +79 -0
- package/src/primitives/skeleton.tsx +55 -0
- package/src/primitives/stat-card.stories.tsx +137 -0
- package/src/primitives/stat-card.tsx +97 -0
- package/src/primitives/switch.stories.tsx +85 -0
- package/src/primitives/switch.tsx +28 -0
- package/src/primitives/table.stories.tsx +170 -0
- package/src/primitives/table.tsx +116 -0
- package/src/primitives/tabs.stories.tsx +180 -0
- package/src/primitives/tabs.tsx +71 -0
- package/src/primitives/terminal-display.stories.tsx +191 -0
- package/src/primitives/terminal-display.tsx +189 -0
- package/src/primitives/theme-toggle.stories.tsx +32 -0
- package/src/primitives/theme-toggle.tsx +96 -0
- package/src/primitives/toast.stories.tsx +155 -0
- package/src/primitives/toast.tsx +190 -0
- package/src/primitives/upload-progress.stories.tsx +120 -0
- package/src/primitives/upload-progress.tsx +110 -0
- package/src/run/expanded-tool-detail.stories.tsx +182 -0
- package/src/run/expanded-tool-detail.tsx +186 -0
- package/src/run/index.ts +13 -0
- package/src/run/inline-thinking-item.stories.tsx +136 -0
- package/src/run/inline-thinking-item.tsx +120 -0
- package/src/run/inline-tool-item.stories.tsx +222 -0
- package/src/run/inline-tool-item.tsx +190 -0
- package/src/run/run-group.stories.tsx +322 -0
- package/src/run/run-group.tsx +569 -0
- package/src/run/run-item-primitives.tsx +17 -0
- package/src/run/tool-call-feed.stories.tsx +294 -0
- package/src/run/tool-call-feed.tsx +192 -0
- package/src/run/tool-call-step.stories.tsx +198 -0
- package/src/run/tool-call-step.tsx +240 -0
- package/src/sdk-hooks.ts +38 -0
- package/src/stores/active-sessions-store.ts +455 -0
- package/src/stores/chat-store.ts +43 -0
- package/src/stores/index.ts +2 -0
- package/src/tool-previews/command-preview.tsx +116 -0
- package/src/tool-previews/diff-preview.tsx +85 -0
- package/src/tool-previews/glob-results-preview.tsx +98 -0
- package/src/tool-previews/grep-results-preview.tsx +157 -0
- package/src/tool-previews/index.ts +22 -0
- package/src/tool-previews/preview-primitives.tsx +84 -0
- package/src/tool-previews/question-preview.tsx +101 -0
- package/src/tool-previews/web-search-preview.tsx +117 -0
- package/src/tool-previews/write-file-preview.tsx +80 -0
- package/src/types/branding.ts +11 -0
- package/src/types/index.ts +5 -0
- package/src/types/message.ts +13 -0
- package/src/types/parts.ts +51 -0
- package/src/types/run.ts +56 -0
- package/src/types/tool-display.ts +41 -0
- package/src/utils/copy-text.ts +30 -0
- package/src/utils/format.test.ts +43 -0
- package/src/utils/format.ts +56 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/time-ago.ts +9 -0
- package/src/utils/tool-display.ts +238 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { HocuspocusProvider } from "@hocuspocus/provider";
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
useCallback,
|
|
8
|
+
useContext,
|
|
9
|
+
useEffect,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from "react";
|
|
14
|
+
import * as Y from "yjs";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Connection state for the Hocuspocus provider.
|
|
18
|
+
*/
|
|
19
|
+
export type ConnectionState =
|
|
20
|
+
| "disconnected"
|
|
21
|
+
| "connecting"
|
|
22
|
+
| "connected"
|
|
23
|
+
| "synced";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Collaborator information from awareness.
|
|
27
|
+
*/
|
|
28
|
+
export interface Collaborator {
|
|
29
|
+
clientId: number;
|
|
30
|
+
user: {
|
|
31
|
+
name: string;
|
|
32
|
+
color: string;
|
|
33
|
+
userId?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Editor context value exposed to children.
|
|
39
|
+
*/
|
|
40
|
+
export interface EditorContextValue {
|
|
41
|
+
/** The Y.Doc instance for collaboration */
|
|
42
|
+
doc: Y.Doc;
|
|
43
|
+
/** The Hocuspocus provider instance */
|
|
44
|
+
provider: HocuspocusProvider | null;
|
|
45
|
+
/** Current connection state */
|
|
46
|
+
connectionState: ConnectionState;
|
|
47
|
+
/** List of active collaborators */
|
|
48
|
+
collaborators: Collaborator[];
|
|
49
|
+
/** Whether the document is synced with the server */
|
|
50
|
+
isSynced: boolean;
|
|
51
|
+
/** Connect to the collaboration server */
|
|
52
|
+
connect: () => void;
|
|
53
|
+
/** Disconnect from the collaboration server */
|
|
54
|
+
disconnect: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const EditorContext = createContext<EditorContextValue | null>(null);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* User information for presence/awareness.
|
|
61
|
+
*/
|
|
62
|
+
export interface EditorUser {
|
|
63
|
+
name: string;
|
|
64
|
+
color?: string;
|
|
65
|
+
userId?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface EditorTokenRefreshResult {
|
|
69
|
+
token: string;
|
|
70
|
+
expiresAt?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Props for EditorProvider.
|
|
75
|
+
*/
|
|
76
|
+
export interface EditorProviderProps {
|
|
77
|
+
/** WebSocket URL for the Hocuspocus server */
|
|
78
|
+
websocketUrl: string;
|
|
79
|
+
/** Document name (e.g., "doc:my-document") */
|
|
80
|
+
documentName: string;
|
|
81
|
+
/** JWT token for authentication */
|
|
82
|
+
token: string;
|
|
83
|
+
/** Unix timestamp (seconds) when the current token expires */
|
|
84
|
+
tokenExpiresAt?: number;
|
|
85
|
+
/** Current user information for awareness */
|
|
86
|
+
user: EditorUser;
|
|
87
|
+
/** Auto-connect on mount (default: true) */
|
|
88
|
+
autoConnect?: boolean;
|
|
89
|
+
/** Reconnect on disconnect (default: true) */
|
|
90
|
+
autoReconnect?: boolean;
|
|
91
|
+
/** Max reconnect attempts (default: 5) */
|
|
92
|
+
maxReconnectAttempts?: number;
|
|
93
|
+
/** Callback when connection state changes */
|
|
94
|
+
onConnectionChange?: (state: ConnectionState) => void;
|
|
95
|
+
/** Callback when sync completes */
|
|
96
|
+
onSync?: () => void;
|
|
97
|
+
/** Callback on authentication error */
|
|
98
|
+
onAuthError?: (error: Error) => void;
|
|
99
|
+
/** Optional token refresh callback used before reconnect/auth retry */
|
|
100
|
+
onRefreshToken?: () => Promise<EditorTokenRefreshResult | string>;
|
|
101
|
+
/** Children components */
|
|
102
|
+
children: ReactNode;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generate a random color for user presence.
|
|
107
|
+
*/
|
|
108
|
+
function generateUserColor(): string {
|
|
109
|
+
const colors = [
|
|
110
|
+
"#FF6B6B", // Red
|
|
111
|
+
"#4ECDC4", // Teal
|
|
112
|
+
"#45B7D1", // Blue
|
|
113
|
+
"#96CEB4", // Green
|
|
114
|
+
"#FFEAA7", // Yellow
|
|
115
|
+
"#DDA0DD", // Plum
|
|
116
|
+
"#98D8C8", // Mint
|
|
117
|
+
"#F7DC6F", // Gold
|
|
118
|
+
"#BB8FCE", // Purple
|
|
119
|
+
"#85C1E9", // Sky
|
|
120
|
+
];
|
|
121
|
+
return colors[Math.floor(Math.random() * colors.length)];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* EditorProvider wraps children with Hocuspocus collaboration context.
|
|
126
|
+
* Manages WebSocket connection, Y.Doc, and awareness state.
|
|
127
|
+
*/
|
|
128
|
+
export function EditorProvider({
|
|
129
|
+
websocketUrl,
|
|
130
|
+
documentName,
|
|
131
|
+
token,
|
|
132
|
+
tokenExpiresAt,
|
|
133
|
+
user,
|
|
134
|
+
autoConnect = true,
|
|
135
|
+
autoReconnect = true,
|
|
136
|
+
maxReconnectAttempts = 5,
|
|
137
|
+
onConnectionChange,
|
|
138
|
+
onSync,
|
|
139
|
+
onAuthError,
|
|
140
|
+
onRefreshToken,
|
|
141
|
+
children,
|
|
142
|
+
}: EditorProviderProps) {
|
|
143
|
+
const [connectionState, setConnectionState] =
|
|
144
|
+
useState<ConnectionState>("disconnected");
|
|
145
|
+
const [collaborators, setCollaborators] = useState<Collaborator[]>([]);
|
|
146
|
+
const [isSynced, setIsSynced] = useState(false);
|
|
147
|
+
|
|
148
|
+
// Use refs to avoid recreating provider on every render
|
|
149
|
+
const docRef = useRef<Y.Doc | null>(null);
|
|
150
|
+
const providerRef = useRef<HocuspocusProvider | null>(null);
|
|
151
|
+
const reconnectAttemptsRef = useRef(0);
|
|
152
|
+
const tokenRef = useRef(token);
|
|
153
|
+
const tokenExpiryRef = useRef<number | undefined>(tokenExpiresAt);
|
|
154
|
+
const refreshPromiseRef = useRef<Promise<string | null> | null>(null);
|
|
155
|
+
const refreshTimerRef = useRef<number | null>(null);
|
|
156
|
+
|
|
157
|
+
tokenRef.current = token;
|
|
158
|
+
tokenExpiryRef.current = tokenExpiresAt;
|
|
159
|
+
|
|
160
|
+
// Initialize Y.Doc once
|
|
161
|
+
if (!docRef.current) {
|
|
162
|
+
docRef.current = new Y.Doc();
|
|
163
|
+
}
|
|
164
|
+
const doc = docRef.current;
|
|
165
|
+
|
|
166
|
+
// User color (generate once per session)
|
|
167
|
+
const userColor = useMemo(
|
|
168
|
+
() => user.color ?? generateUserColor(),
|
|
169
|
+
[user.color],
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Update connection state and notify callback
|
|
173
|
+
const updateConnectionState = useCallback(
|
|
174
|
+
(state: ConnectionState) => {
|
|
175
|
+
setConnectionState(state);
|
|
176
|
+
onConnectionChange?.(state);
|
|
177
|
+
},
|
|
178
|
+
[onConnectionChange],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Update collaborators from awareness
|
|
182
|
+
const updateCollaborators = useCallback(
|
|
183
|
+
(awareness: HocuspocusProvider["awareness"]) => {
|
|
184
|
+
if (!awareness) return;
|
|
185
|
+
|
|
186
|
+
const states = awareness.getStates();
|
|
187
|
+
const collabs: Collaborator[] = [];
|
|
188
|
+
|
|
189
|
+
states.forEach((state: Record<string, unknown>, clientId: number) => {
|
|
190
|
+
// Skip our own client
|
|
191
|
+
if (clientId === awareness.clientID) return;
|
|
192
|
+
|
|
193
|
+
if (state.user) {
|
|
194
|
+
collabs.push({
|
|
195
|
+
clientId,
|
|
196
|
+
user: state.user as Collaborator["user"],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
setCollaborators(collabs);
|
|
202
|
+
},
|
|
203
|
+
[],
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const clearRefreshTimer = useCallback(() => {
|
|
207
|
+
if (refreshTimerRef.current != null) {
|
|
208
|
+
window.clearTimeout(refreshTimerRef.current);
|
|
209
|
+
refreshTimerRef.current = null;
|
|
210
|
+
}
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
const refreshToken = useCallback(async (): Promise<string | null> => {
|
|
214
|
+
if (!onRefreshToken) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (refreshPromiseRef.current) {
|
|
219
|
+
return refreshPromiseRef.current;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const refreshPromise = (async () => {
|
|
223
|
+
const next = await onRefreshToken();
|
|
224
|
+
const resolvedToken = typeof next === "string" ? next : next.token;
|
|
225
|
+
const resolvedExpiry =
|
|
226
|
+
typeof next === "string" ? undefined : next.expiresAt;
|
|
227
|
+
|
|
228
|
+
tokenRef.current = resolvedToken;
|
|
229
|
+
tokenExpiryRef.current = resolvedExpiry;
|
|
230
|
+
return resolvedToken;
|
|
231
|
+
})()
|
|
232
|
+
.catch((error) => {
|
|
233
|
+
onAuthError?.(
|
|
234
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
235
|
+
);
|
|
236
|
+
return null;
|
|
237
|
+
})
|
|
238
|
+
.finally(() => {
|
|
239
|
+
refreshPromiseRef.current = null;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
refreshPromiseRef.current = refreshPromise;
|
|
243
|
+
return refreshPromise;
|
|
244
|
+
}, [onAuthError, onRefreshToken]);
|
|
245
|
+
|
|
246
|
+
const scheduleTokenRefresh = useCallback(() => {
|
|
247
|
+
clearRefreshTimer();
|
|
248
|
+
|
|
249
|
+
if (
|
|
250
|
+
!tokenExpiryRef.current ||
|
|
251
|
+
!onRefreshToken ||
|
|
252
|
+
typeof window === "undefined"
|
|
253
|
+
) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const refreshAtMs = tokenExpiryRef.current * 1000 - 60_000;
|
|
258
|
+
const delay = refreshAtMs - Date.now();
|
|
259
|
+
|
|
260
|
+
if (delay <= 0) {
|
|
261
|
+
void refreshToken();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
refreshTimerRef.current = window.setTimeout(() => {
|
|
266
|
+
void refreshToken();
|
|
267
|
+
}, delay);
|
|
268
|
+
}, [clearRefreshTimer, onRefreshToken, refreshToken]);
|
|
269
|
+
|
|
270
|
+
// Connect to the collaboration server
|
|
271
|
+
const connect = useCallback(() => {
|
|
272
|
+
if (providerRef.current) {
|
|
273
|
+
providerRef.current.connect();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
updateConnectionState("connecting");
|
|
278
|
+
|
|
279
|
+
const provider = new HocuspocusProvider({
|
|
280
|
+
url: websocketUrl,
|
|
281
|
+
name: documentName,
|
|
282
|
+
document: doc,
|
|
283
|
+
token: async () => tokenRef.current,
|
|
284
|
+
connect: true,
|
|
285
|
+
|
|
286
|
+
onConnect: () => {
|
|
287
|
+
reconnectAttemptsRef.current = 0;
|
|
288
|
+
updateConnectionState("connected");
|
|
289
|
+
scheduleTokenRefresh();
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
onSynced: () => {
|
|
293
|
+
setIsSynced(true);
|
|
294
|
+
updateConnectionState("synced");
|
|
295
|
+
onSync?.();
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
onDisconnect: () => {
|
|
299
|
+
updateConnectionState("disconnected");
|
|
300
|
+
setIsSynced(false);
|
|
301
|
+
clearRefreshTimer();
|
|
302
|
+
|
|
303
|
+
// Auto-reconnect logic
|
|
304
|
+
if (
|
|
305
|
+
autoReconnect &&
|
|
306
|
+
reconnectAttemptsRef.current < maxReconnectAttempts
|
|
307
|
+
) {
|
|
308
|
+
reconnectAttemptsRef.current += 1;
|
|
309
|
+
const delay = Math.min(
|
|
310
|
+
1000 * 2 ** reconnectAttemptsRef.current,
|
|
311
|
+
30000,
|
|
312
|
+
);
|
|
313
|
+
setTimeout(() => {
|
|
314
|
+
if (providerRef.current && !(providerRef.current as any).isConnected) {
|
|
315
|
+
providerRef.current.connect();
|
|
316
|
+
}
|
|
317
|
+
}, delay);
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
onAuthenticationFailed: ({ reason }: { reason?: string }) => {
|
|
322
|
+
const error = new Error(reason ?? "Authentication failed");
|
|
323
|
+
updateConnectionState("disconnected");
|
|
324
|
+
clearRefreshTimer();
|
|
325
|
+
|
|
326
|
+
if (onRefreshToken) {
|
|
327
|
+
void refreshToken().then((nextToken) => {
|
|
328
|
+
if (nextToken && providerRef.current) {
|
|
329
|
+
providerRef.current.connect();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
onAuthError?.(error);
|
|
334
|
+
});
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
onAuthError?.(error);
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
onAwarenessUpdate: () => {
|
|
342
|
+
updateCollaborators(provider.awareness);
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Set local user awareness
|
|
347
|
+
provider.awareness?.setLocalStateField("user", {
|
|
348
|
+
name: user.name,
|
|
349
|
+
color: userColor,
|
|
350
|
+
userId: user.userId,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
providerRef.current = provider;
|
|
354
|
+
}, [
|
|
355
|
+
websocketUrl,
|
|
356
|
+
documentName,
|
|
357
|
+
doc,
|
|
358
|
+
user.name,
|
|
359
|
+
user.userId,
|
|
360
|
+
userColor,
|
|
361
|
+
autoReconnect,
|
|
362
|
+
maxReconnectAttempts,
|
|
363
|
+
clearRefreshTimer,
|
|
364
|
+
updateConnectionState,
|
|
365
|
+
updateCollaborators,
|
|
366
|
+
onSync,
|
|
367
|
+
onAuthError,
|
|
368
|
+
onRefreshToken,
|
|
369
|
+
refreshToken,
|
|
370
|
+
scheduleTokenRefresh,
|
|
371
|
+
]);
|
|
372
|
+
|
|
373
|
+
// Disconnect from the collaboration server
|
|
374
|
+
const disconnect = useCallback(() => {
|
|
375
|
+
if (providerRef.current) {
|
|
376
|
+
providerRef.current.disconnect();
|
|
377
|
+
updateConnectionState("disconnected");
|
|
378
|
+
}
|
|
379
|
+
}, [updateConnectionState]);
|
|
380
|
+
|
|
381
|
+
// Auto-connect on mount
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
if (autoConnect) {
|
|
384
|
+
connect();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return () => {
|
|
388
|
+
// Cleanup on unmount
|
|
389
|
+
clearRefreshTimer();
|
|
390
|
+
if (providerRef.current) {
|
|
391
|
+
providerRef.current.destroy();
|
|
392
|
+
providerRef.current = null;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
}, [autoConnect, clearRefreshTimer, connect]);
|
|
396
|
+
|
|
397
|
+
// Context value
|
|
398
|
+
const contextValue = useMemo<EditorContextValue>(
|
|
399
|
+
() => ({
|
|
400
|
+
doc,
|
|
401
|
+
provider: providerRef.current,
|
|
402
|
+
connectionState,
|
|
403
|
+
collaborators,
|
|
404
|
+
isSynced,
|
|
405
|
+
connect,
|
|
406
|
+
disconnect,
|
|
407
|
+
}),
|
|
408
|
+
[doc, connectionState, collaborators, isSynced, connect, disconnect],
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
<EditorContext.Provider value={contextValue}>
|
|
413
|
+
{children}
|
|
414
|
+
</EditorContext.Provider>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Hook to access the editor context.
|
|
420
|
+
* Must be used within an EditorProvider.
|
|
421
|
+
*/
|
|
422
|
+
export function useEditorContext(): EditorContextValue {
|
|
423
|
+
const context = useContext(EditorContext);
|
|
424
|
+
if (!context) {
|
|
425
|
+
throw new Error("useEditorContext must be used within an EditorProvider");
|
|
426
|
+
}
|
|
427
|
+
return context;
|
|
428
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type Editor } from "@tiptap/react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Editor toolbar component.
|
|
8
|
+
* Provides basic formatting controls.
|
|
9
|
+
*/
|
|
10
|
+
export function EditorToolbar({
|
|
11
|
+
editor,
|
|
12
|
+
className,
|
|
13
|
+
}: {
|
|
14
|
+
editor: Editor | null;
|
|
15
|
+
className?: string;
|
|
16
|
+
}) {
|
|
17
|
+
if (!editor) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const buttons = [
|
|
22
|
+
{
|
|
23
|
+
id: "bold",
|
|
24
|
+
label: "Bold",
|
|
25
|
+
action: () => editor.chain().focus().toggleBold().run(),
|
|
26
|
+
isActive: editor.isActive("bold"),
|
|
27
|
+
shortcut: "Ctrl+B",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "italic",
|
|
31
|
+
label: "Italic",
|
|
32
|
+
action: () => editor.chain().focus().toggleItalic().run(),
|
|
33
|
+
isActive: editor.isActive("italic"),
|
|
34
|
+
shortcut: "Ctrl+I",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "strike",
|
|
38
|
+
label: "Strike",
|
|
39
|
+
action: () => editor.chain().focus().toggleStrike().run(),
|
|
40
|
+
isActive: editor.isActive("strike"),
|
|
41
|
+
shortcut: "Ctrl+Shift+X",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "code",
|
|
45
|
+
label: "Code",
|
|
46
|
+
action: () => editor.chain().focus().toggleCode().run(),
|
|
47
|
+
isActive: editor.isActive("code"),
|
|
48
|
+
shortcut: "Ctrl+E",
|
|
49
|
+
},
|
|
50
|
+
{ id: "sep-1", type: "separator" as const },
|
|
51
|
+
{
|
|
52
|
+
id: "h1",
|
|
53
|
+
label: "H1",
|
|
54
|
+
action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
|
55
|
+
isActive: editor.isActive("heading", { level: 1 }),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "h2",
|
|
59
|
+
label: "H2",
|
|
60
|
+
action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
61
|
+
isActive: editor.isActive("heading", { level: 2 }),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "h3",
|
|
65
|
+
label: "H3",
|
|
66
|
+
action: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
67
|
+
isActive: editor.isActive("heading", { level: 3 }),
|
|
68
|
+
},
|
|
69
|
+
{ id: "sep-2", type: "separator" as const },
|
|
70
|
+
{
|
|
71
|
+
id: "bullet-list",
|
|
72
|
+
label: "Bullet List",
|
|
73
|
+
action: () => editor.chain().focus().toggleBulletList().run(),
|
|
74
|
+
isActive: editor.isActive("bulletList"),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "ordered-list",
|
|
78
|
+
label: "Ordered List",
|
|
79
|
+
action: () => editor.chain().focus().toggleOrderedList().run(),
|
|
80
|
+
isActive: editor.isActive("orderedList"),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "code-block",
|
|
84
|
+
label: "Code Block",
|
|
85
|
+
action: () => editor.chain().focus().toggleCodeBlock().run(),
|
|
86
|
+
isActive: editor.isActive("codeBlock"),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "blockquote",
|
|
90
|
+
label: "Blockquote",
|
|
91
|
+
action: () => editor.chain().focus().toggleBlockquote().run(),
|
|
92
|
+
isActive: editor.isActive("blockquote"),
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
className={cn(
|
|
99
|
+
"flex items-center gap-1 border-border border-b bg-card p-2",
|
|
100
|
+
className,
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
{buttons.map((button) => {
|
|
104
|
+
if ("type" in button && button.type === "separator") {
|
|
105
|
+
return <div key={button.id} className="mx-1 h-6 w-px bg-border" />;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<button
|
|
110
|
+
key={button.id}
|
|
111
|
+
onClick={button.action}
|
|
112
|
+
type="button"
|
|
113
|
+
title={
|
|
114
|
+
"shortcut" in button
|
|
115
|
+
? `${button.label} (${button.shortcut})`
|
|
116
|
+
: button.label
|
|
117
|
+
}
|
|
118
|
+
className={cn(
|
|
119
|
+
"rounded px-2 py-1 font-medium text-xs transition-colors",
|
|
120
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
121
|
+
button.isActive && "bg-accent text-accent-foreground",
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{button.label}
|
|
125
|
+
</button>
|
|
126
|
+
);
|
|
127
|
+
})}
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export {
|
|
2
|
+
EditorProvider,
|
|
3
|
+
useEditorContext,
|
|
4
|
+
type EditorProviderProps,
|
|
5
|
+
type EditorContextValue,
|
|
6
|
+
type EditorTokenRefreshResult,
|
|
7
|
+
type EditorUser,
|
|
8
|
+
type ConnectionState,
|
|
9
|
+
type Collaborator,
|
|
10
|
+
} from "./editor-provider";
|
|
11
|
+
export {
|
|
12
|
+
TiptapEditor,
|
|
13
|
+
CollaboratorsList,
|
|
14
|
+
EditorToolbar,
|
|
15
|
+
type TiptapEditorProps,
|
|
16
|
+
} from "./tiptap-editor";
|
|
17
|
+
export {
|
|
18
|
+
DocumentEditorPane,
|
|
19
|
+
type DocumentEditorPaneProps,
|
|
20
|
+
type DocumentEditorMode,
|
|
21
|
+
type DocumentEditorBackend,
|
|
22
|
+
type DocumentEditorPaneCollaborationConfig,
|
|
23
|
+
} from "./document-editor-pane";
|
|
24
|
+
export {
|
|
25
|
+
useEditorConnection,
|
|
26
|
+
useCollaborators,
|
|
27
|
+
useCollaboratorPresence,
|
|
28
|
+
useYjsState,
|
|
29
|
+
useDocumentChanges,
|
|
30
|
+
useAwareness,
|
|
31
|
+
} from "./use-editor";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { marked } from "marked";
|
|
2
|
+
import TurndownService from "turndown";
|
|
3
|
+
|
|
4
|
+
const turndown = new TurndownService({
|
|
5
|
+
bulletListMarker: "-",
|
|
6
|
+
codeBlockStyle: "fenced",
|
|
7
|
+
emDelimiter: "*",
|
|
8
|
+
headingStyle: "atx",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function markdownToHtml(markdown: string) {
|
|
12
|
+
return String(marked.parse(markdown, { async: false, gfm: true }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function htmlToMarkdown(html: string) {
|
|
16
|
+
return turndown.turndown(html);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeMarkdown(markdown: string) {
|
|
20
|
+
return markdown.replace(/\r\n/g, "\n").trimEnd();
|
|
21
|
+
}
|