total-recall-brain 3.41.4 → 3.41.6

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.
Files changed (147) hide show
  1. package/bin/total-recall.mjs +10 -6
  2. package/docs/reference/cli-reference.md +18 -0
  3. package/frontend/dist/assets/index-4tGQfTa2.js +5605 -0
  4. package/frontend/dist/assets/index-DtwsDyFM.css +1 -0
  5. package/frontend/dist/index.html +2 -2
  6. package/frontend/package.json +11 -0
  7. package/frontend/src/App.tsx +47 -122
  8. package/frontend/src/components/plugins/plugin-frame.ts +1 -0
  9. package/frontend/src/index.css +152 -86
  10. package/frontend/src/pages/HelpPage.tsx +13 -10
  11. package/frontend/src/pages/MemoryPage.tsx +28 -30
  12. package/frontend/src/pages/NetworkPage.tsx +3 -3
  13. package/frontend/src/pages/PluginsPage.tsx +6 -5
  14. package/frontend/src/pages/RulesPage.tsx +16 -12
  15. package/package.json +4 -3
  16. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +3114 -36
  17. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +4 -1
  18. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +3114 -36
  19. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +4 -1
  20. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +3121 -33
  21. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +4 -1
  22. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +3122 -34
  23. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +4 -1
  24. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +3127 -49
  25. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -2
  26. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +1 -1
  27. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +136 -7
  28. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +135 -26
  29. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +3121 -33
  30. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +5 -2
  31. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +228 -9
  32. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +24 -30
  33. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +29 -16
  34. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +73 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +182 -365
  36. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -0
  37. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +40 -14
  38. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +4 -4
  39. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/agy.mjs +187 -0
  40. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/claude.mjs +275 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cline.mjs +132 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/codex.mjs +187 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cursor.mjs +158 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/devin.mjs +157 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/dsh.mjs +183 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/grok.mjs +132 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +173 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +87 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/openclaw.mjs +173 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/pi.mjs +129 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +12 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/json-reply.mjs +22 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +72 -70
  54. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-catalog.mjs +195 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +11 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +36 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +169 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +136 -346
  59. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +1 -1
  60. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/ssss-store.mjs +20 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/telemetry.mjs +43 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +156 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +117 -35
  64. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +51 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +398 -57
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +304 -46
  67. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +367 -41
  68. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +5 -2
  69. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +33 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +2 -2
  71. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +159 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +197 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +153 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +261 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +151 -4
  76. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +114 -0
  77. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +96 -0
  78. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +899 -662
  79. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +8987 -0
  80. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/mermaid-diagrams/SKILL.md +44 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/write-connector/SKILL.md +56 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3097 -81
  83. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +4 -1
  84. package/src/cli/agent.mjs +12 -1
  85. package/src/cli/compact.mjs +108 -0
  86. package/src/cli/connect.mjs +56 -1
  87. package/src/cli/context.mjs +55 -23
  88. package/src/cli/doctor.mjs +5 -2
  89. package/src/cli/forget.mjs +2 -0
  90. package/src/cli/harness.mjs +62 -50
  91. package/src/cli/memory-entry.mjs +2 -2
  92. package/src/cli/preflight.mjs +152 -16
  93. package/src/cli/release.mjs +386 -0
  94. package/src/cli/remember.mjs +19 -1
  95. package/src/cli/rules.mjs +54 -12
  96. package/src/cli/startup.mjs +1 -1
  97. package/src/cli/status.mjs +12 -2
  98. package/src/core/claude-hooks.mjs +41 -3
  99. package/src/core/cli-commands.mjs +2 -0
  100. package/src/core/cli-semantic-index.mjs +3 -2
  101. package/src/core/conflict-detector.mjs +23 -12
  102. package/src/core/context-compactor.mjs +145 -0
  103. package/src/core/context-policy.mjs +20 -0
  104. package/src/core/duplicate-guard.mjs +350 -0
  105. package/src/core/meta-harness.mjs +31 -11
  106. package/src/core/preflight-gate.mjs +2 -2
  107. package/src/core/preflight-receipt.mjs +29 -1
  108. package/src/core/scaffold-plugins.mjs +7 -0
  109. package/src/core/stdio-flush.mjs +23 -0
  110. package/src/core/surface.mjs +33 -10
  111. package/src/server/invoke-durability.mjs +39 -0
  112. package/src/server/invoke-write-child.mjs +25 -0
  113. package/src/server/routes/invoke.mjs +7 -0
  114. package/src/server/routes/plugins.mjs +2 -2
  115. package/frontend/dist/assets/index-BIQmqDKP.css +0 -1
  116. package/frontend/dist/assets/index-CRzjQLu7.js +0 -5631
  117. package/frontend/src/components/DaemonLogsTab.tsx +0 -54
  118. package/frontend/src/components/ResearchAgendaTab.tsx +0 -712
  119. package/frontend/src/components/TaskQueueTab.tsx +0 -193
  120. package/frontend/src/components/VoiceInput.tsx +0 -112
  121. package/frontend/src/pages/ChatPage.tsx +0 -1059
  122. package/frontend/src/pages/CollabPage.tsx +0 -470
  123. package/frontend/src/pages/DesignDocsPage.tsx +0 -621
  124. package/frontend/src/pages/FilesPage.tsx +0 -393
  125. package/frontend/src/pages/HealthPage.tsx +0 -359
  126. package/frontend/src/pages/InboxPage.tsx +0 -83
  127. package/frontend/src/pages/InstructionsPage.tsx +0 -583
  128. package/frontend/src/pages/IntegrationsPage.tsx +0 -761
  129. package/frontend/src/pages/MeshPage.css +0 -58
  130. package/frontend/src/pages/MeshPage.tsx +0 -1219
  131. package/frontend/src/pages/OkfPage.tsx +0 -535
  132. package/frontend/src/pages/SandboxPage.tsx +0 -113
  133. package/frontend/src/pages/SkillsPage.tsx +0 -1994
  134. package/frontend/src/pages/TasksPage.tsx +0 -427
  135. package/frontend/src/pages/UsagePage.tsx +0 -702
  136. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +0 -65
  137. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +0 -76
  138. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +0 -19
  139. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -266
  140. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +0 -446
  141. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +0 -152
  142. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +0 -71
  143. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +0 -36
  144. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +0 -35
  145. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +0 -175
  146. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +0 -32
  147. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +0 -575
@@ -1,470 +0,0 @@
1
- import React, { useState, useEffect, useRef } from 'react';
2
-
3
- // ─── Interfaces ──────────────────────────────────────────────────────────────
4
- export interface CollabGroup {
5
- id: string;
6
- name: string;
7
- code: string;
8
- owner: string;
9
- members: string[];
10
- created_at: string;
11
- }
12
-
13
- interface Annotation {
14
- id: string;
15
- url: string;
16
- groupCode: string;
17
- author: string;
18
- text: string;
19
- excerpt: string;
20
- created_at: string;
21
- }
22
-
23
- interface ChatMessage {
24
- username: string;
25
- text: string;
26
- created_at: string;
27
- }
28
-
29
- // ─── Component: AuthView ───────────────────────────────────────────────────────
30
- function CollabAuthView({ onAuthSuccess }: { onAuthSuccess: (token: string, username: string) => void }) {
31
- const [isLogin, setIsLogin] = useState(true);
32
- const [username, setUsername] = useState('');
33
- const [password, setPassword] = useState('');
34
- const [error, setError] = useState('');
35
- const [loading, setLoading] = useState(false);
36
-
37
- const handleSubmit = async (e: React.FormEvent) => {
38
- e.preventDefault();
39
- if (!username || !password) {
40
- setError('Please fill in all fields');
41
- return;
42
- }
43
- setError('');
44
- setLoading(true);
45
- const endpoint = isLogin ? '/api/collab/auth/login' : '/api/collab/auth/register';
46
-
47
- try {
48
- const response = await fetch(endpoint, {
49
- method: 'POST',
50
- headers: { 'Content-Type': 'application/json' },
51
- body: JSON.stringify({ username, password }),
52
- });
53
- const data = await response.json();
54
- if (!response.ok) throw new Error(data.error || 'Authentication failed');
55
- onAuthSuccess(data.token, data.username);
56
- } catch (err: unknown) {
57
- setError((err as Error).message || 'Connection error');
58
- } finally {
59
- setLoading(false);
60
- }
61
- };
62
-
63
- return (
64
- <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh', padding: '20px' }}>
65
- <div className="glass-panel" style={{ width: '100%', maxWidth: '400px', padding: '30px', background: 'rgba(255,255,255,0.02)', border: '1px solid var(--border)', borderRadius: '12px' }}>
66
- <h2 style={{ fontSize: '22px', marginBottom: '8px', textAlign: 'center' }}>Total Recall Collaboration</h2>
67
- <p style={{ fontSize: '13px', color: 'var(--text-secondary)', textAlign: 'center', marginBottom: '24px' }}>Sign in to connect with team workspaces</p>
68
- <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
69
- <div>
70
- <label style={{ display: 'block', fontSize: '12px', color: 'var(--text-secondary)', marginBottom: '6px' }}>Username</label>
71
- <input type="text" value={username} onChange={e => setUsername(e.target.value)} disabled={loading} style={{ background: 'rgba(0,0,0,0.2)', border: '1px solid var(--border)', color: '#fff', padding: '8px 12px', borderRadius: '6px', width: '100%' }} />
72
- </div>
73
- <div>
74
- <label style={{ display: 'block', fontSize: '12px', color: 'var(--text-secondary)', marginBottom: '6px' }}>Password</label>
75
- <input type="password" value={password} onChange={e => setPassword(e.target.value)} disabled={loading} style={{ background: 'rgba(0,0,0,0.2)', border: '1px solid var(--border)', color: '#fff', padding: '8px 12px', borderRadius: '6px', width: '100%' }} />
76
- </div>
77
- {error && <div style={{ padding: '8px', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.2)', borderRadius: '6px', color: '#ef4444', fontSize: '12px', textAlign: 'center' }}>{error}</div>}
78
- <button type="submit" className="btn btn-purple" style={{ width: '100%', padding: '10px' }} disabled={loading}>
79
- {loading ? 'Processing...' : isLogin ? 'Access Collab Hub' : 'Register Collab Account'}
80
- </button>
81
- </form>
82
- <div style={{ marginTop: '20px', textAlign: 'center', fontSize: '13px' }}>
83
- <button onClick={() => { setIsLogin(!isLogin); setError(''); }} style={{ background: 'none', border: 'none', color: 'var(--purple-primary)', cursor: 'pointer' }}>
84
- {isLogin ? 'Create an account instead' : 'Already have an account? Login'}
85
- </button>
86
- </div>
87
- </div>
88
- </div>
89
- );
90
- }
91
-
92
- // ─── Component: CollabDashboard ───────────────────────────────────────────────
93
- function CollabDashboard({ token, username, onLogout, onSelectSandbox }: { token: string, username: string, onLogout: () => void, onSelectSandbox: () => void }) {
94
- const [groups, setGroups] = useState<CollabGroup[]>([]);
95
- const [newGroupName, setNewGroupName] = useState('');
96
- const [joinCode, setJoinCode] = useState('');
97
- const [error, setError] = useState('');
98
- const [success, setSuccess] = useState('');
99
- const [copiedCode, setCopiedCode] = useState<string | null>(null); const [refreshTrigger, setRefreshTrigger] = useState(0);
100
-
101
- useEffect(() => {
102
- let active = true;
103
- const fetchGroups = async () => {
104
- try {
105
- const response = await fetch('/api/collab/groups', {
106
- headers: { Authorization: `Bearer ${token}` },
107
- });
108
- if (response.ok && active) {
109
- setGroups(await response.json());
110
- }
111
- } catch (err) {
112
- console.error(err);
113
- }
114
- };
115
- fetchGroups();
116
- return () => { active = false; };
117
- }, [token, refreshTrigger]);
118
-
119
- const handleCreate = async (e: React.FormEvent) => {
120
- e.preventDefault();
121
- if (!newGroupName.trim()) return;
122
- setError(''); setSuccess('');
123
- try {
124
- const response = await fetch('/api/collab/groups', {
125
- method: 'POST',
126
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
127
- body: JSON.stringify({ name: newGroupName }),
128
- });
129
- const data = await response.json();
130
- if (!response.ok) throw new Error(data.error);
131
- setSuccess(`Group "${data.name}" created!`);
132
- setNewGroupName('');
133
- setRefreshTrigger(prev => prev + 1);
134
- } catch (err: unknown) { setError((err as Error).message); }
135
- };
136
-
137
- const handleJoin = async (e: React.FormEvent) => {
138
- e.preventDefault();
139
- if (!joinCode.trim()) return;
140
- setError(''); setSuccess('');
141
- try {
142
- const response = await fetch('/api/collab/groups/join', {
143
- method: 'POST',
144
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
145
- body: JSON.stringify({ code: joinCode }),
146
- });
147
- const data = await response.json();
148
- if (!response.ok) throw new Error(data.error);
149
- setSuccess(`Joined group "${data.group.name}"!`);
150
- setJoinCode('');
151
- setRefreshTrigger(prev => prev + 1);
152
- } catch (err: unknown) { setError((err as Error).message); }
153
- };
154
-
155
- const copyCode = (code: string) => {
156
- navigator.clipboard.writeText(code);
157
- setCopiedCode(code);
158
- setTimeout(() => setCopiedCode(null), 2000);
159
- };
160
-
161
- return (
162
- <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
163
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: 'rgba(255,255,255,0.02)', padding: '16px 20px', borderRadius: '8px', border: '1px solid var(--border)' }}>
164
- <div>
165
- <h2 style={{ fontSize: '18px' }}>Collab Spaces for <span style={{ color: 'var(--purple-primary)' }}>{username}</span></h2>
166
- <p style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>Share annotations and real-time page chat rooms</p>
167
- </div>
168
- <div style={{ display: 'flex', gap: '10px' }}>
169
- <button className="btn btn-purple" onClick={onSelectSandbox}>🌐 Open Browse Workspace</button>
170
- <button className="btn btn-ghost" onClick={onLogout} style={{ color: '#f87171' }}>Logout</button>
171
- </div>
172
- </div>
173
-
174
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 300px', gap: '20px' }}>
175
- <div style={{ background: 'rgba(255,255,255,0.01)', border: '1px solid var(--border)', padding: '20px', borderRadius: '8px' }}>
176
- <h3 style={{ fontSize: '15px', marginBottom: '16px' }}>My Workspaces ({groups.length})</h3>
177
- {groups.length === 0 ? (
178
- <p style={{ color: 'var(--text-secondary)', fontSize: '13px' }}>No collaboration groups yet. Create or join one.</p>
179
- ) : (
180
- <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
181
- {groups.map(g => (
182
- <div key={g.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: 'rgba(255,255,255,0.02)', padding: '12px 16px', borderRadius: '6px', border: '1px solid var(--border)' }}>
183
- <div>
184
- <h4 style={{ fontSize: '14px', color: '#fff' }}>{g.name}</h4>
185
- <p style={{ fontSize: '11px', color: 'var(--text-secondary)' }}>Members: {g.members.length} • Invite Code: <strong style={{ color: 'var(--purple-primary)' }}>{g.code}</strong></p>
186
- </div>
187
- <button className="btn btn-ghost btn-sm" onClick={() => copyCode(g.code)}>{copiedCode === g.code ? 'Copied' : 'Copy Code'}</button>
188
- </div>
189
- ))}
190
- </div>
191
- )}
192
- </div>
193
-
194
- <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
195
- <div style={{ background: 'rgba(255,255,255,0.01)', border: '1px solid var(--border)', padding: '16px', borderRadius: '8px' }}>
196
- <h3 style={{ fontSize: '14px', marginBottom: '12px' }}>Join a Space</h3>
197
- <form onSubmit={handleJoin} style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
198
- <input type="text" placeholder="Invite Code" value={joinCode} onChange={e => setJoinCode(e.target.value)} style={{ background: 'rgba(0,0,0,0.2)', border: '1px solid var(--border)', color: '#fff', padding: '6px 10px', borderRadius: '6px', fontSize: '13px' }} />
199
- <button type="submit" className="btn btn-purple btn-sm">Join Group</button>
200
- </form>
201
- </div>
202
-
203
- <div style={{ background: 'rgba(255,255,255,0.01)', border: '1px solid var(--border)', padding: '16px', borderRadius: '8px' }}>
204
- <h3 style={{ fontSize: '14px', marginBottom: '12px' }}>Create a Space</h3>
205
- <form onSubmit={handleCreate} style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
206
- <input type="text" placeholder="Space Name" value={newGroupName} onChange={e => setNewGroupName(e.target.value)} style={{ background: 'rgba(0,0,0,0.2)', border: '1px solid var(--border)', color: '#fff', padding: '6px 10px', borderRadius: '6px', fontSize: '13px' }} />
207
- <button type="submit" className="btn btn-purple btn-sm">Create Group</button>
208
- </form>
209
- </div>
210
-
211
- {(error || success) && <div style={{ textAlign: 'center', padding: '8px', fontSize: '12px', color: error ? '#f87171' : '#34d399', background: 'rgba(255,255,255,0.01)', border: '1px solid var(--border)', borderRadius: '6px' }}>{error || success}</div>}
212
- </div>
213
- </div>
214
- </div>
215
- );
216
- }
217
-
218
- // ─── Component: CollabSandbox ─────────────────────────────────────────────────
219
- function CollabSandbox({ token, username, onBack }: { token: string, username: string, onBack: () => void }) {
220
- const [groups, setGroups] = useState<CollabGroup[]>([]);
221
- const [selectedGroup, setSelectedGroup] = useState<CollabGroup | null>(null);
222
- const [activeBrowseUrl, setActiveBrowseUrl] = useState('https://news.ycombinator.com');
223
- const [activeUrl, setActiveUrl] = useState('https://news.ycombinator.com');
224
- const [annotations, setAnnotations] = useState<Annotation[]>([]);
225
- const [noteText, setNoteText] = useState('');
226
- const [noteExcerpt, setNoteExcerpt] = useState('');
227
- const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
228
- const [chatInput, setChatInput] = useState('');
229
- const [activeUsers, setActiveUsers] = useState<string[]>([]);
230
- const [systemLogs, setSystemLogs] = useState<string[]>([]);
231
- const [wsConnected, setWsConnected] = useState(false);
232
-
233
- const socketRef = useRef<WebSocket | null>(null);
234
- const chatEndRef = useRef<HTMLDivElement | null>(null);
235
-
236
- useEffect(() => {
237
- const fetchGroups = async () => {
238
- try {
239
- const response = await fetch('/api/collab/groups', {
240
- headers: { Authorization: `Bearer ${token}` },
241
- });
242
- if (response.ok) {
243
- const data = await response.json();
244
- setGroups(data);
245
- if (data.length > 0) setSelectedGroup(data[0]);
246
- }
247
- } catch (err) {
248
- console.error(err);
249
- }
250
- };
251
- fetchGroups();
252
- }, [token]); const [refreshTrigger, setRefreshTrigger] = useState(0);
253
-
254
- useEffect(() => {
255
- let active = true;
256
- const fetchAnnotations = async () => {
257
- try {
258
- const response = await fetch(`/api/collab/annotations?url=${encodeURIComponent(activeUrl)}`, {
259
- headers: { Authorization: `Bearer ${token}` },
260
- });
261
- if (response.ok && active) {
262
- setAnnotations(await response.json());
263
- }
264
- } catch (err) {
265
- console.error(err);
266
- }
267
- };
268
- fetchAnnotations();
269
- return () => { active = false; };
270
- }, [activeUrl, token, groups, refreshTrigger]);
271
-
272
- useEffect(() => {
273
- const wsProto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
274
- const wsHost = window.location.host;
275
- const wsUrl = `${wsProto}//${wsHost}/collab-ws?token=${token}`;
276
- const ws = new WebSocket(wsUrl);
277
- socketRef.current = ws;
278
-
279
- ws.onopen = () => {
280
- setWsConnected(true);
281
- ws.send(JSON.stringify({ type: 'SUBSCRIBE', url: activeUrl }));
282
- setSystemLogs(prev => [...prev, `Subscribed to context channel: ${activeUrl}`]);
283
- };
284
-
285
- ws.onmessage = (event) => {
286
- try {
287
- const data = JSON.parse(event.data);
288
- if (data.type === 'CHAT_MESSAGE') {
289
- setChatMessages(prev => [...prev, data]);
290
- } else if (data.type === 'USER_JOINED') {
291
- setSystemLogs(prev => [...prev, `${data.username} joined this page`]);
292
- setActiveUsers(prev => prev.includes(data.username) ? prev : [...prev, data.username]);
293
- } else if (data.type === 'USER_LEFT') {
294
- setSystemLogs(prev => [...prev, `${data.username} left`]);
295
- setActiveUsers(prev => prev.filter(u => u !== data.username));
296
- } else if (data.type === 'ANNOTATION_ADDED') {
297
- setRefreshTrigger(prev => prev + 1);
298
- }
299
- } catch (err) {
300
- console.error(err);
301
- }
302
- };
303
-
304
- ws.onclose = () => {
305
- setWsConnected(false);
306
- setSystemLogs(prev => [...prev, 'Disconnected.']);
307
- };
308
-
309
- return () => { ws.close(); };
310
- }, [activeUrl, token]);
311
-
312
- useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [chatMessages, systemLogs]);
313
-
314
- const handleUrlSubmit = (e: React.FormEvent) => {
315
- e.preventDefault();
316
- if (!activeBrowseUrl.trim()) return;
317
- setChatMessages([]); setSystemLogs([]); setActiveUsers([]);
318
- setActiveUrl(activeBrowseUrl);
319
- };
320
-
321
- const handleAddAnnotation = async (e: React.FormEvent) => {
322
- e.preventDefault();
323
- if (!noteText.trim() || !selectedGroup) return;
324
- try {
325
- const response = await fetch('/api/collab/annotations', {
326
- method: 'POST',
327
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
328
- body: JSON.stringify({ url: activeUrl, groupCode: selectedGroup.code, text: noteText, excerpt: noteExcerpt }),
329
- });
330
- if (response.ok) {
331
- setNoteText(''); setNoteExcerpt('');
332
- setRefreshTrigger(prev => prev + 1);
333
- }
334
- } catch (err) {
335
- console.error(err);
336
- }
337
- };
338
-
339
- const handleSendChat = (e: React.FormEvent) => {
340
- e.preventDefault();
341
- if (!chatInput.trim() || !socketRef.current || socketRef.current.readyState !== WebSocket.OPEN) return;
342
- socketRef.current.send(JSON.stringify({ type: 'CHAT_MESSAGE', text: chatInput }));
343
- setChatInput('');
344
- };
345
-
346
- return (
347
- <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
348
- <div style={{ display: 'flex', gap: '16px', alignItems: 'center', justifyContent: 'space-between', background: 'rgba(255,255,255,0.02)', padding: '12px 20px', borderRadius: '8px', border: '1px solid var(--border)', flexWrap: 'wrap' }}>
349
- <button className="btn btn-ghost" onClick={onBack}>← Back to Workspaces</button>
350
- <form onSubmit={handleUrlSubmit} style={{ display: 'flex', gap: '10px', flex: '1', minWidth: '250px' }}>
351
- <input type="text" value={activeBrowseUrl} onChange={e => setActiveBrowseUrl(e.target.value)} style={{ background: 'rgba(0,0,0,0.2)', border: '1px solid var(--border)', color: '#fff', padding: '6px 12px', borderRadius: '6px', flex: '1', fontFamily: 'monospace' }} />
352
- <button type="submit" className="btn btn-purple">Connect URL</button>
353
- </form>
354
- <div style={{ fontSize: '13px', color: wsConnected ? '#34d399' : '#f87171' }}>● {wsConnected ? 'Connected' : 'Disconnected'}</div>
355
- </div>
356
-
357
- <div style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
358
- Active browse context URL: <strong style={{ color: 'var(--purple-primary)', fontFamily: 'monospace' }}>{activeUrl}</strong>
359
- </div>
360
-
361
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 340px', gap: '20px', height: '500px' }}>
362
- {/* Annotations */}
363
- <div style={{ background: 'rgba(255,255,255,0.01)', border: '1px solid var(--border)', borderRadius: '8px', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
364
- <div style={{ flex: '1', padding: '16px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '10px' }}>
365
- <h3 style={{ fontSize: '14px', marginBottom: '8px' }}>Persistent Notes on Web Page</h3>
366
- {annotations.length === 0 ? (
367
- <p style={{ color: 'var(--text-secondary)', fontSize: '13px' }}>No notes pinned on this page context.</p>
368
- ) : (
369
- annotations.map(ann => (
370
- <div key={ann.id} style={{ background: 'rgba(255,255,255,0.02)', padding: '10px 14px', borderRadius: '6px', border: '1px solid var(--border)' }}>
371
- <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '11px', color: 'var(--text-secondary)', marginBottom: '4px' }}>
372
- <span>By <strong>{ann.author}</strong></span>
373
- <span>{new Date(ann.created_at).toLocaleTimeString()}</span>
374
- </div>
375
- {ann.excerpt && <blockquote style={{ borderLeft: '2px solid var(--purple-primary)', paddingLeft: '8px', fontSize: '12px', color: 'var(--text-secondary)', margin: '4px 0 8px 0', background: 'rgba(0,0,0,0.1)', padding: '4px' }}>"{ann.excerpt}"</blockquote>}
376
- <p style={{ fontSize: '13px', color: '#fff' }}>{ann.text}</p>
377
- </div>
378
- ))
379
- )}
380
- </div>
381
-
382
- <form onSubmit={handleAddAnnotation} style={{ borderTop: '1px solid var(--border)', padding: '12px', background: 'rgba(0,0,0,0.1)', display: 'flex', flexDirection: 'column', gap: '8px' }}>
383
- <div style={{ display: 'flex', gap: '8px' }}>
384
- <select value={selectedGroup?.code || ''} onChange={e => setSelectedGroup(groups.find(g => g.code === e.target.value) || null)} style={{ background: 'rgba(0,0,0,0.4)', border: '1px solid var(--border)', color: '#fff', padding: '6px', borderRadius: '6px', fontSize: '12px' }}>
385
- {groups.map(g => <option key={g.code} value={g.code}>{g.name}</option>)}
386
- </select>
387
- <input type="text" placeholder="Excerpt (optional context)" value={noteExcerpt} onChange={e => setNoteExcerpt(e.target.value)} style={{ background: 'rgba(0,0,0,0.2)', border: '1px solid var(--border)', color: '#fff', padding: '6px 10px', borderRadius: '6px', flex: '1', fontSize: '12px' }} />
388
- </div>
389
- <div style={{ display: 'flex', gap: '8px' }}>
390
- <input type="text" placeholder="Add annotation comment..." value={noteText} onChange={e => setNoteText(e.target.value)} required style={{ background: 'rgba(0,0,0,0.2)', border: '1px solid var(--border)', color: '#fff', padding: '6px 10px', borderRadius: '6px', flex: '1', fontSize: '12px' }} />
391
- <button type="submit" className="btn btn-purple btn-sm" disabled={groups.length === 0}>Pin</button>
392
- </div>
393
- </form>
394
- </div>
395
-
396
- {/* Chat Panel */}
397
- <div style={{ background: 'rgba(255,255,255,0.01)', border: '1px solid var(--border)', borderRadius: '8px', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
398
- <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border)', background: 'rgba(0,0,0,0.1)' }}>
399
- <h3 style={{ fontSize: '13px' }}>Live Web Room {activeUsers.length > 0 && <span style={{ fontSize: '11px', color: 'var(--text-secondary)' }}>({activeUsers.join(', ')})</span>}</h3>
400
- </div>
401
- <div style={{ flex: '1', padding: '12px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '8px' }}>
402
- {systemLogs.map((log, idx) => <div key={idx} style={{ fontSize: '10px', color: 'var(--text-secondary)', fontStyle: 'italic' }}>ℹ️ {log}</div>)}
403
- {chatMessages.map((msg, idx) => {
404
- const isSelf = msg.username.toLowerCase() === username.toLowerCase();
405
- return (
406
- <div key={idx} style={{ display: 'flex', flexDirection: 'column', alignSelf: isSelf ? 'flex-end' : 'flex-start', maxWidth: '85%' }}>
407
- <span style={{ fontSize: '9px', color: 'var(--text-secondary)', alignSelf: isSelf ? 'flex-end' : 'flex-start' }}>{msg.username} • {new Date(msg.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
408
- <div style={{ padding: '8px 12px', borderRadius: '8px', background: isSelf ? 'var(--purple-primary)' : 'rgba(255,255,255,0.04)', fontSize: '12px', color: '#fff', wordBreak: 'break-word', marginTop: '2px' }}>{msg.text}</div>
409
- </div>
410
- );
411
- })}
412
- <div ref={chatEndRef} />
413
- </div>
414
- <form onSubmit={handleSendChat} style={{ borderTop: '1px solid var(--border)', padding: '10px', display: 'flex', gap: '6px' }}>
415
- <input type="text" placeholder="Send chat..." value={chatInput} onChange={e => setChatInput(e.target.value)} required disabled={!wsConnected} style={{ background: 'rgba(0,0,0,0.2)', border: '1px solid var(--border)', color: '#fff', padding: '6px 10px', borderRadius: '6px', flex: '1', fontSize: '12px' }} />
416
- <button type="submit" className="btn btn-purple btn-sm" disabled={!wsConnected}>Send</button>
417
- </form>
418
- </div>
419
- </div>
420
- </div>
421
- );
422
- }
423
-
424
- // ─── Main Integrated CollabPage Component ─────────────────────────────────────
425
- export default function CollabPage() {
426
- const [token, setToken] = useState<string | null>(() => localStorage.getItem('collab_token'));
427
- const [username, setUsername] = useState<string | null>(() => localStorage.getItem('collab_username'));
428
- const [view, setView] = useState<'login' | 'dashboard' | 'sandbox'>(() => {
429
- const savedToken = localStorage.getItem('collab_token');
430
- const savedUser = localStorage.getItem('collab_username');
431
- return (savedToken && savedUser) ? 'dashboard' : 'login';
432
- });
433
-
434
- const handleAuthSuccess = (newToken: string, newUsername: string) => {
435
- localStorage.setItem('collab_token', newToken);
436
- localStorage.setItem('collab_username', newUsername);
437
- setToken(newToken);
438
- setUsername(newUsername);
439
- setView('dashboard');
440
- };
441
-
442
- const handleLogout = () => {
443
- localStorage.removeItem('collab_token');
444
- localStorage.removeItem('collab_username');
445
- setToken(null);
446
- setUsername(null);
447
- setView('login');
448
- };
449
-
450
- return (
451
- <div className="page" style={{ padding: '24px', maxWidth: '1100px', margin: '0 auto' }}>
452
- {view === 'login' && <CollabAuthView onAuthSuccess={handleAuthSuccess} />}
453
- {view === 'dashboard' && token && username && (
454
- <CollabDashboard
455
- token={token}
456
- username={username}
457
- onLogout={handleLogout}
458
- onSelectSandbox={() => setView('sandbox')}
459
- />
460
- )}
461
- {view === 'sandbox' && token && username && (
462
- <CollabSandbox
463
- token={token}
464
- username={username}
465
- onBack={() => setView('dashboard')}
466
- />
467
- )}
468
- </div>
469
- );
470
- }