@polderlabs/bizar 4.8.0 → 5.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.
Files changed (129) hide show
  1. package/bizar-dash/dist/assets/icons-CFqu2M-c.js +656 -0
  2. package/bizar-dash/dist/assets/icons-CFqu2M-c.js.map +1 -0
  3. package/bizar-dash/dist/assets/index-DmpSFPJY.js +9 -0
  4. package/bizar-dash/dist/assets/index-DmpSFPJY.js.map +1 -0
  5. package/bizar-dash/dist/assets/main-Dl8yY5_H.js +16 -0
  6. package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +1 -0
  7. package/bizar-dash/dist/assets/{main-DX_Jh8Wc.css → main-ZAfGKENE.css} +1 -1
  8. package/bizar-dash/dist/assets/markdown-DIquRulQ.js +29 -0
  9. package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +1 -0
  10. package/bizar-dash/dist/assets/mobile-C2gysFOZ.js +2 -0
  11. package/bizar-dash/dist/assets/mobile-C2gysFOZ.js.map +1 -0
  12. package/bizar-dash/dist/assets/mobile-DHXXbn1A.js +1 -0
  13. package/bizar-dash/dist/assets/{mobile-Chvf9u_B.js.map → mobile-DHXXbn1A.js.map} +1 -1
  14. package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js +40 -0
  15. package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +1 -0
  16. package/bizar-dash/dist/index.html +6 -3
  17. package/bizar-dash/dist/mobile.html +5 -2
  18. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  19. package/bizar-dash/skills/eval/SKILL.md +237 -0
  20. package/bizar-dash/src/server/api.mjs +28 -0
  21. package/bizar-dash/src/server/auth.mjs +155 -1
  22. package/bizar-dash/src/server/eval-store.mjs +226 -0
  23. package/bizar-dash/src/server/eval.mjs +347 -0
  24. package/bizar-dash/src/server/memory-lightrag.mjs +109 -0
  25. package/bizar-dash/src/server/memory-store.mjs +121 -0
  26. package/bizar-dash/src/server/ocr.mjs +55 -0
  27. package/bizar-dash/src/server/otel.mjs +133 -0
  28. package/bizar-dash/src/server/plugins/registry.mjs +363 -0
  29. package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
  30. package/bizar-dash/src/server/plugins/store.mjs +659 -0
  31. package/bizar-dash/src/server/routes/chat.mjs +246 -170
  32. package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
  33. package/bizar-dash/src/server/routes/eval.mjs +147 -0
  34. package/bizar-dash/src/server/routes/memory.mjs +46 -0
  35. package/bizar-dash/src/server/routes/ocr.mjs +182 -0
  36. package/bizar-dash/src/server/routes/opencode-sessions.mjs +82 -48
  37. package/bizar-dash/src/server/routes/plugins.mjs +220 -0
  38. package/bizar-dash/src/server/routes/users.mjs +84 -0
  39. package/bizar-dash/src/server/routes/voice.mjs +131 -0
  40. package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
  41. package/bizar-dash/src/server/server.mjs +40 -0
  42. package/bizar-dash/src/server/voice-store.mjs +202 -0
  43. package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
  44. package/bizar-dash/src/server/workspaces.mjs +626 -0
  45. package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
  46. package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
  47. package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
  48. package/bizar-dash/src/web/components/SettingsSearch.tsx +204 -89
  49. package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
  50. package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
  51. package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
  52. package/bizar-dash/src/web/lib/search.ts +115 -0
  53. package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +10 -35
  54. package/bizar-dash/src/web/mobile/views/QrCodePanel.tsx +69 -0
  55. package/bizar-dash/src/web/styles/memory.css +166 -1
  56. package/bizar-dash/src/web/styles/settings.css +80 -0
  57. package/bizar-dash/src/web/views/Memory.tsx +22 -2
  58. package/bizar-dash/src/web/views/Settings.tsx +99 -0
  59. package/bizar-dash/src/web/views/Workspace.tsx +294 -0
  60. package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
  61. package/bizar-dash/src/web/views/memory/MemoryGraphLegend.tsx +29 -0
  62. package/bizar-dash/src/web/views/memory/MemoryGraphPanel.tsx +192 -0
  63. package/bizar-dash/src/web/views/memory/MemoryGraphView.tsx +336 -0
  64. package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
  65. package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
  66. package/bizar-dash/tests/bundle-analysis.test.mjs +73 -0
  67. package/bizar-dash/tests/clipboard.test.mjs +147 -0
  68. package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
  69. package/bizar-dash/tests/components/settings-search.test.tsx +180 -0
  70. package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
  71. package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
  72. package/bizar-dash/tests/docker-build.test.mjs +96 -0
  73. package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
  74. package/bizar-dash/tests/eval/report.test.mjs +284 -0
  75. package/bizar-dash/tests/eval/runner.test.mjs +471 -0
  76. package/bizar-dash/tests/lib/search-fuzzy.test.ts +149 -0
  77. package/bizar-dash/tests/memory-graph-view.test.tsx +69 -0
  78. package/bizar-dash/tests/memory-graph.test.mjs +95 -0
  79. package/bizar-dash/tests/ocr.test.mjs +87 -0
  80. package/bizar-dash/tests/otel.test.mjs +188 -0
  81. package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
  82. package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
  83. package/bizar-dash/tests/plugins-store.test.mjs +455 -0
  84. package/bizar-dash/tests/users.test.mjs +108 -0
  85. package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
  86. package/bizar-dash/tests/voice-store.test.mjs +148 -0
  87. package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
  88. package/bizar-dash/tests/workspaces.test.mjs +527 -0
  89. package/cli/bin.mjs +72 -2
  90. package/cli/commands/clip.mjs +146 -0
  91. package/cli/commands/dash.mjs +6 -0
  92. package/cli/commands/deploy/cloudflare.mjs +250 -0
  93. package/cli/commands/deploy/docker.mjs +221 -0
  94. package/cli/commands/deploy/fly.mjs +161 -0
  95. package/cli/commands/deploy/vercel.mjs +225 -0
  96. package/cli/commands/deploy.mjs +240 -0
  97. package/cli/commands/eval.mjs +378 -0
  98. package/cli/commands/marketplace.mjs +64 -0
  99. package/cli/commands/ocr.mjs +165 -0
  100. package/cli/commands/plugin.mjs +358 -0
  101. package/cli/commands/voice.mjs +211 -0
  102. package/cli/commands/workspace.mjs +247 -0
  103. package/package.json +12 -2
  104. package/templates/deploy/cloudflare/README.md +32 -0
  105. package/templates/deploy/cloudflare/functions-index.template.js +15 -0
  106. package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
  107. package/templates/deploy/docker/.env.template +16 -0
  108. package/templates/deploy/docker/README.md +58 -0
  109. package/templates/deploy/docker/docker-compose.template.yml +23 -0
  110. package/templates/deploy/fly/README.md +35 -0
  111. package/templates/deploy/fly/fly.toml.template +28 -0
  112. package/templates/deploy/vercel/README.md +29 -0
  113. package/templates/deploy/vercel/api-index.template.js +18 -0
  114. package/templates/deploy/vercel/vercel.json.template +16 -0
  115. package/templates/eval-fixtures/README.md +58 -0
  116. package/templates/eval-fixtures/code-search-basic.json +28 -0
  117. package/templates/eval-fixtures/latency-bounds.json +16 -0
  118. package/templates/eval-fixtures/regression-suite.json +79 -0
  119. package/templates/eval-fixtures/response-format.json +30 -0
  120. package/templates/eval-fixtures/tool-call-correctness.json +24 -0
  121. package/templates/plugin-template/README.md +121 -0
  122. package/templates/plugin-template/index.js +66 -0
  123. package/templates/plugin-template/plugin.json +42 -0
  124. package/templates/plugin-template/tests/plugin.test.js +83 -0
  125. package/bizar-dash/dist/assets/main-DHZmbnxQ.js +0 -361
  126. package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +0 -1
  127. package/bizar-dash/dist/assets/mobile-BK8-ythT.js +0 -351
  128. package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +0 -1
  129. package/bizar-dash/dist/assets/mobile-Chvf9u_B.js +0 -1
@@ -10,10 +10,14 @@
10
10
  import React, { useCallback, useState } from 'react';
11
11
  import {
12
12
  Brain,
13
+ Clipboard,
13
14
  FileText,
14
15
  GitBranch,
15
16
  LayoutDashboard,
17
+ Mic,
18
+ Network,
16
19
  RefreshCw,
20
+ Scan,
17
21
  Search as SearchIcon,
18
22
  Sliders,
19
23
  } from 'lucide-react';
@@ -26,8 +30,12 @@ import { ObsidianPanel } from './memory/ObsidianPanel';
26
30
  import { GitSyncPanel } from './memory/GitSyncPanel';
27
31
  import { SemanticSearchPanel } from './memory/SemanticSearchPanel';
28
32
  import { ConfigPanel } from './memory/ConfigPanel';
33
+ import { MemoryGraphPanel } from './memory/MemoryGraphPanel';
34
+ import { FromScreenshotPanel } from './memory/FromScreenshotPanel';
35
+ import { VaultFromClipboardPanel } from './memory/VaultFromClipboardPanel';
36
+ import { VoiceNotesPanel } from '../components/VoiceNotesPanel';
29
37
 
30
- type SubPanel = 'overview' | 'lightrag' | 'obsidian' | 'git' | 'semantic' | 'config';
38
+ type SubPanel = 'overview' | 'lightrag' | 'obsidian' | 'git' | 'semantic' | 'config' | 'graph' | 'webclip' | 'screenshot' | 'voice';
31
39
 
32
40
  type Props = {
33
41
  snapshot: unknown;
@@ -48,6 +56,10 @@ const SOURCES: Array<{
48
56
  { id: 'git', label: 'Git Sync', icon: GitBranch },
49
57
  { id: 'semantic', label: 'Semantic Search', icon: SearchIcon },
50
58
  { id: 'config', label: 'Config', icon: Sliders },
59
+ { id: 'graph', label: 'Memory Graph', icon: Network },
60
+ { id: 'webclip', label: 'Web Clip', icon: Clipboard },
61
+ { id: 'screenshot', label: 'Screenshot OCR', icon: Scan },
62
+ { id: 'voice', label: 'Voice Notes', icon: Mic },
51
63
  ];
52
64
 
53
65
  function MemoryInner(_props: Props) {
@@ -78,6 +90,14 @@ function MemoryInner(_props: Props) {
78
90
  return <SemanticSearchPanel refreshKey={refreshKey} />;
79
91
  case 'config':
80
92
  return <ConfigPanel refreshKey={refreshKey} />;
93
+ case 'graph':
94
+ return <MemoryGraphPanel refreshKey={refreshKey} />;
95
+ case 'webclip':
96
+ return <VaultFromClipboardPanel refreshKey={refreshKey} />;
97
+ case 'screenshot':
98
+ return <FromScreenshotPanel refreshKey={refreshKey} />;
99
+ case 'voice':
100
+ return <VoiceNotesPanel refreshKey={refreshKey} />;
81
101
  default:
82
102
  return <Card>Unknown panel: {active}</Card>;
83
103
  }
@@ -92,7 +112,7 @@ function MemoryInner(_props: Props) {
92
112
  <Brain size={18} /> Memory
93
113
  </h2>
94
114
  <p className="view-subtitle">
95
- LightRAG, Obsidian vault, git sync, and semantic search — all in one place.
115
+ LightRAG, Obsidian vault, git sync, semantic search, web clips, and screenshot OCR — all in one place.
96
116
  </p>
97
117
  </div>
98
118
  <div className="view-actions">
@@ -7,6 +7,7 @@ import { api } from '../lib/api';
7
7
  import { cn } from '../lib/utils';
8
8
  import { applyTheme, applyThemeTokens, type Settings, type SettingsResponse, type Snapshot, type TailscaleStatus } from '../lib/types';
9
9
 
10
+ import { SettingsSearch, type SettingsSection, type SettingsField } from '../components/SettingsSearch';
10
11
  import { ThemeSection } from './settings/ThemeSection';
11
12
  import { UpdatesSection } from './settings/UpdatesSection';
12
13
  import { GeneralSection } from './settings/GeneralSection';
@@ -22,6 +23,7 @@ import { ProvidersSection } from './settings/ProvidersSection';
22
23
  import { MemorySection } from './settings/MemorySection';
23
24
  import { SkillsSection } from './settings/SkillsSection';
24
25
  import { BackupSection } from './settings/BackupSection';
26
+ import { WorkspacesSection } from './settings/WorkspacesSection';
25
27
 
26
28
  type Props = {
27
29
  snapshot: Snapshot;
@@ -51,8 +53,102 @@ const SECTION_LINKS = [
51
53
  { id: 'memory', label: 'Memory' },
52
54
  { id: 'skills', label: 'Skills' },
53
55
  { id: 'backup', label: 'Backup' },
56
+ { id: 'workspaces', label: 'Workspaces' },
54
57
  ] as const;
55
58
 
59
+ /* ─── Settings search sections metadata ─── */
60
+ const SETTINGS_SECTIONS: SettingsSection[] = [
61
+ { id: 'theme', label: 'Theme', fields: [
62
+ { key: 'theme.presets', label: 'Accent presets', section: 'theme' },
63
+ { key: 'theme.accent', label: 'Accent color', section: 'theme' },
64
+ { key: 'theme.success', label: 'Success color', section: 'theme' },
65
+ { key: 'theme.warning', label: 'Warning color', section: 'theme' },
66
+ { key: 'theme.error', label: 'Error color', section: 'theme' },
67
+ { key: 'theme.info', label: 'Info color', section: 'theme' },
68
+ { key: 'theme.fontFamily', label: 'Font family', section: 'theme' },
69
+ { key: 'theme.fontSize', label: 'Font size', section: 'theme' },
70
+ { key: 'theme.compactMode', label: 'Compact mode', section: 'theme' },
71
+ { key: 'theme.animations', label: 'Animations', section: 'theme' },
72
+ ] },
73
+ { id: 'updates', label: 'Updates', fields: [
74
+ { key: 'updates.channel', label: 'Update channel', section: 'updates' },
75
+ ] },
76
+ { id: 'layout', label: 'Layout', fields: [
77
+ { key: 'ui.layout', label: 'UI layout', section: 'layout' },
78
+ { key: 'ui.showHeader', label: 'Show header', section: 'layout' },
79
+ { key: 'ui.showStatusBar', label: 'Show status bar', section: 'layout' },
80
+ { key: 'ui.defaultTab', label: 'Default tab', section: 'layout' },
81
+ ] },
82
+ { id: 'general', label: 'General', fields: [
83
+ { key: 'defaultAgent', label: 'Default agent', section: 'general' },
84
+ { key: 'defaultModel', label: 'Model override', section: 'general' },
85
+ ] },
86
+ { id: 'network', label: 'Network', fields: [
87
+ { key: 'tailscale.enabled', label: 'Tailscale Serve', section: 'network' },
88
+ { key: 'tailscale.port', label: 'Tailscale port', section: 'network' },
89
+ { key: 'tailscale.https', label: 'Tailscale HTTPS', section: 'network' },
90
+ { key: 'tailscale.hostname', label: 'Tailscale hostname', section: 'network' },
91
+ ] },
92
+ { id: 'notifications', label: 'Notifications', fields: [
93
+ { key: 'notifications.onAgentComplete', label: 'Notify on agent complete', section: 'notifications' },
94
+ { key: 'notifications.onPlanApproval', label: 'Notify on plan approval', section: 'notifications' },
95
+ ] },
96
+ { id: 'auth', label: 'Auth', fields: [
97
+ { key: 'auth.enabled', label: 'Auth enabled', section: 'auth' },
98
+ { key: 'auth.loopback', label: 'Loopback mode', section: 'auth' },
99
+ { key: 'auth.token', label: 'Auth token', section: 'auth' },
100
+ ] },
101
+ { id: 'agents', label: 'Agents', fields: [
102
+ { key: 'agents.maxParallel', label: 'Max parallel agents', section: 'agents' },
103
+ { key: 'agents.stuckThresholdMs', label: 'Stuck threshold', section: 'agents' },
104
+ { key: 'agents.autoRestart', label: 'Auto restart', section: 'agents' },
105
+ { key: 'workflow.artifactsEnabled', label: 'Artifacts enabled', section: 'agents' },
106
+ { key: 'workflow.agentsDecideAutonomously', label: 'Autonomous decisions', section: 'agents' },
107
+ ] },
108
+ { id: 'dashboard', label: 'Dashboard', fields: [
109
+ { key: 'dashboard.autoLaunchWeb', label: 'Auto-launch web', section: 'dashboard' },
110
+ { key: 'dashboard.projectsDirectory', label: 'Projects directory', section: 'dashboard' },
111
+ { key: 'dashboard.allowedRoots', label: 'Allowed roots', section: 'dashboard' },
112
+ ] },
113
+ { id: 'background', label: 'Background', fields: [
114
+ { key: 'service.enabled', label: 'Background service', section: 'background' },
115
+ { key: 'service.autostart', label: 'Auto-start service', section: 'background' },
116
+ ] },
117
+ { id: 'system-llm', label: 'System LLM', fields: [
118
+ { key: 'systemLlm.enabled', label: 'System LLM enabled', section: 'system-llm' },
119
+ ] },
120
+ { id: 'headroom', label: 'Headroom', fields: [
121
+ { key: 'headroom.enabled', label: 'Headroom enabled', section: 'headroom' },
122
+ { key: 'headroom.port', label: 'Headroom port', section: 'headroom' },
123
+ { key: 'headroom.budget', label: 'Headroom budget', section: 'headroom' },
124
+ { key: 'headroom.backend', label: 'Headroom backend', section: 'headroom' },
125
+ ] },
126
+ { id: 'activity-log', label: 'Activity', fields: [
127
+ { key: 'activity.log', label: 'Activity log', section: 'activity-log' },
128
+ ] },
129
+ { id: 'about', label: 'About', fields: [
130
+ { key: 'about.version', label: 'Version', section: 'about' },
131
+ { key: 'about.homepage', label: 'Homepage', section: 'about' },
132
+ { key: 'about.license', label: 'License', section: 'about' },
133
+ ] },
134
+ { id: 'env-vars', label: 'Env Vars', fields: [
135
+ { key: 'env.add', label: 'Add variable', section: 'env-vars' },
136
+ ] },
137
+ { id: 'providers', label: 'Providers', fields: [
138
+ { key: 'providers.list', label: 'Provider list', section: 'providers' },
139
+ ] },
140
+ { id: 'memory', label: 'Memory', fields: [
141
+ { key: 'memory.config', label: 'Memory config', section: 'memory' },
142
+ ] },
143
+ { id: 'skills', label: 'Skills', fields: [
144
+ { key: 'skills.paths', label: 'Skill paths', section: 'skills' },
145
+ ] },
146
+ { id: 'backup', label: 'Backup', fields: [
147
+ { key: 'backup.create', label: 'Create backup', section: 'backup' },
148
+ { key: 'backup.restore', label: 'Restore backup', section: 'backup' },
149
+ ] },
150
+ ];
151
+
56
152
  function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
57
153
  const toast = useToast();
58
154
  const [settings, setSettings] = useState<Settings>(initial);
@@ -145,6 +241,8 @@ function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
145
241
  </div>
146
242
  </header>
147
243
 
244
+ <SettingsSearch sections={SETTINGS_SECTIONS} onJump={onJumpSection} />
245
+
148
246
  <nav className="settings-subnav" aria-label="Settings sections">
149
247
  <button type="button" className={cn('settings-subnav-button', 'settings-subnav-button-all', showAll && 'settings-subnav-button-active')} onClick={() => onJumpSection(null)} title="Show all settings sections">All</button>
150
248
  {SECTION_LINKS.map((s) => (
@@ -168,6 +266,7 @@ function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
168
266
  {(showAll || sectionOf('memory')) && <MemorySection />}
169
267
  {(showAll || sectionOf('skills')) && <SkillsSection />}
170
268
  {(showAll || sectionOf('backup')) && <BackupSection />}
269
+ {(showAll || sectionOf('workspaces')) && <WorkspacesSection />}
171
270
  </div>
172
271
  </div>
173
272
  );
@@ -0,0 +1,294 @@
1
+ // src/web/views/Workspace.tsx — v5.0.0 — workspace management view.
2
+ import { useEffect, useState, useCallback } from 'react';
3
+ import { Users, Plus, Trash2, Shield, Crown, Eye } from 'lucide-react';
4
+ import { api } from '../lib/api';
5
+ import { useModal } from '../components/Modal';
6
+ import { Button } from '../components/Button';
7
+ import { InviteDialog, type InviteInfo } from '../components/InviteDialog';
8
+ import { cn } from '../lib/utils';
9
+
10
+ export type WorkspaceMember = {
11
+ userId: string;
12
+ email: string;
13
+ name: string;
14
+ role: 'admin' | 'editor' | 'viewer';
15
+ joinedAt: string;
16
+ };
17
+
18
+ export type WorkspaceInfo = {
19
+ id: string;
20
+ name: string;
21
+ ownerId: string;
22
+ createdAt: string;
23
+ };
24
+
25
+ type Props = {
26
+ workspaceId: string;
27
+ onWorkspaceDeleted?: () => void;
28
+ onWorkspaceUpdated?: (name: string) => void;
29
+ };
30
+
31
+ const ROLE_ICONS = {
32
+ admin: Crown,
33
+ editor: Shield,
34
+ viewer: Eye,
35
+ };
36
+
37
+ const ROLE_COLORS = {
38
+ admin: 'role-admin',
39
+ editor: 'role-editor',
40
+ viewer: 'role-viewer',
41
+ };
42
+
43
+ export function WorkspaceView({ workspaceId, onWorkspaceDeleted, onWorkspaceUpdated }: Props) {
44
+ const modal = useModal();
45
+ const [workspace, setWorkspace] = useState<WorkspaceInfo | null>(null);
46
+ const [members, setMembers] = useState<WorkspaceMember[]>([]);
47
+ const [invites, setInvites] = useState<InviteInfo[]>([]);
48
+ const [loading, setLoading] = useState(true);
49
+ const [error, setError] = useState<string | null>(null);
50
+ const [nameDraft, setNameDraft] = useState('');
51
+ const [nameEditing, setNameEditing] = useState(false);
52
+
53
+ const loadWorkspace = useCallback(async () => {
54
+ setLoading(true);
55
+ setError(null);
56
+ try {
57
+ const r = await api.get<{ workspace: WorkspaceInfo; members: WorkspaceMember[] }>(
58
+ `/workspaces/${workspaceId}`,
59
+ );
60
+ setWorkspace(r.workspace);
61
+ setMembers(r.members || []);
62
+ setNameDraft(r.workspace.name);
63
+ } catch (err: unknown) {
64
+ setError((err as Error).message || 'Failed to load workspace');
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ }, [workspaceId]);
69
+
70
+ useEffect(() => {
71
+ loadWorkspace();
72
+ }, [loadWorkspace]);
73
+
74
+ const loadInvites = async () => {
75
+ try {
76
+ const r = await api.get<{ invites: InviteInfo[] }>(`/workspaces/${workspaceId}/invites`);
77
+ setInvites(r.invites || []);
78
+ } catch {
79
+ // Ignore
80
+ }
81
+ };
82
+
83
+ useEffect(() => {
84
+ loadInvites();
85
+ }, [workspaceId]);
86
+
87
+ const handleSaveName = async () => {
88
+ if (!nameDraft.trim() || !workspace) return;
89
+ try {
90
+ // Workspace rename would be a separate endpoint; for now just update locally
91
+ setWorkspace((w) => (w ? { ...w, name: nameDraft.trim() } : w));
92
+ onWorkspaceUpdated?.(nameDraft.trim());
93
+ setNameEditing(false);
94
+ } catch {
95
+ setError('Failed to update name');
96
+ }
97
+ };
98
+
99
+ const handleDeleteWorkspace = async () => {
100
+ if (!confirm(`Delete workspace "${workspace?.name}"? This cannot be undone.`)) return;
101
+ if (!confirm('Are you absolutely sure? All workspace data will be permanently deleted.')) return;
102
+ try {
103
+ await api.del(`/workspaces/${workspaceId}`);
104
+ onWorkspaceDeleted?.();
105
+ } catch (err: unknown) {
106
+ setError((err as Error).message || 'Failed to delete workspace');
107
+ }
108
+ };
109
+
110
+ const handleRemoveMember = async (userId: string) => {
111
+ if (!confirm('Remove this member from the workspace?')) return;
112
+ try {
113
+ await api.del(`/workspaces/${workspaceId}/members/${userId}`);
114
+ setMembers((prev) => prev.filter((m) => m.userId !== userId));
115
+ } catch (err: unknown) {
116
+ setError((err as Error).message || 'Failed to remove member');
117
+ }
118
+ };
119
+
120
+ const handleUpdateRole = async (userId: string, newRole: string) => {
121
+ try {
122
+ await api.post(`/workspaces/${workspaceId}/members/${userId}`, { role: newRole });
123
+ setMembers((prev) =>
124
+ prev.map((m) => (m.userId === userId ? { ...m, role: newRole as WorkspaceMember['role'] } : m)),
125
+ );
126
+ } catch (err: unknown) {
127
+ setError((err as Error).message || 'Failed to update role');
128
+ }
129
+ };
130
+
131
+ // Note: In a real implementation, we'd get the current user ID from the session/JWT.
132
+ // For now, we trust the server to only render this view for authorized users.
133
+ const currentUserIsAdmin = true;
134
+
135
+ if (loading) {
136
+ return (
137
+ <div className="view view-workspace">
138
+ <div className="workspace-loading">Loading workspace...</div>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ if (error && !workspace) {
144
+ return (
145
+ <div className="view view-workspace">
146
+ <div className="workspace-error">
147
+ <p>{error}</p>
148
+ <Button variant="secondary" size="sm" onClick={loadWorkspace}>
149
+ Retry
150
+ </Button>
151
+ </div>
152
+ </div>
153
+ );
154
+ }
155
+
156
+ return (
157
+ <div className="view view-workspace">
158
+ <header className="view-header">
159
+ <div className="view-header-text">
160
+ <h2 className="view-title">
161
+ <Users size={18} />
162
+ {nameEditing ? (
163
+ <input
164
+ type="text"
165
+ className="workspace-name-input"
166
+ value={nameDraft}
167
+ onChange={(e) => setNameDraft(e.target.value)}
168
+ onBlur={handleSaveName}
169
+ onKeyDown={(e) => {
170
+ if (e.key === 'Enter') handleSaveName();
171
+ if (e.key === 'Escape') setNameEditing(false);
172
+ }}
173
+ autoFocus
174
+ />
175
+ ) : (
176
+ <span onClick={() => currentUserIsAdmin && setNameEditing(true)} className={cn('workspace-name', currentUserIsAdmin && 'workspace-name-editable')}>
177
+ {workspace?.name}
178
+ </span>
179
+ )}
180
+ </h2>
181
+ <p className="view-subtitle">
182
+ Created {workspace?.createdAt ? new Date(workspace.createdAt).toLocaleDateString() : '—'}
183
+ </p>
184
+ </div>
185
+ <div className="view-actions">
186
+ {currentUserIsAdmin && (
187
+ <>
188
+ <Button
189
+ variant="secondary"
190
+ size="sm"
191
+ onClick={() => {
192
+ modal.open({
193
+ title: 'Invite Members',
194
+ children: <InviteDialog workspaceId={workspaceId} onInviteCreated={loadInvites} />,
195
+ width: 480,
196
+ });
197
+ }}
198
+ >
199
+ <Plus size={14} />
200
+ Invite
201
+ </Button>
202
+ <Button variant="danger" size="sm" onClick={handleDeleteWorkspace}>
203
+ <Trash2 size={14} />
204
+ Delete
205
+ </Button>
206
+ </>
207
+ )}
208
+ </div>
209
+ </header>
210
+
211
+ {error && (
212
+ <div className="workspace-error-banner">
213
+ <span>{error}</span>
214
+ <button onClick={() => setError(null)}>×</button>
215
+ </div>
216
+ )}
217
+
218
+ <div className="workspace-sections">
219
+ {/* Members section */}
220
+ <section className="workspace-section">
221
+ <h3 className="workspace-section-title">Members ({members.length})</h3>
222
+ <div className="workspace-members-list">
223
+ {members.map((member) => {
224
+ const RoleIcon = ROLE_ICONS[member.role] || Eye;
225
+ return (
226
+ <div key={member.userId} className="workspace-member">
227
+ <div className="workspace-member-info">
228
+ <div className="workspace-member-avatar">
229
+ {member.name?.[0]?.toUpperCase() || member.email[0].toUpperCase()}
230
+ </div>
231
+ <div className="workspace-member-details">
232
+ <span className="workspace-member-name">{member.name || 'Unknown'}</span>
233
+ <span className="workspace-member-email">{member.email}</span>
234
+ </div>
235
+ </div>
236
+ <div className="workspace-member-actions">
237
+ <span className={cn('workspace-member-role', ROLE_COLORS[member.role])}>
238
+ <RoleIcon size={12} />
239
+ {member.role}
240
+ </span>
241
+ {currentUserIsAdmin && member.role !== 'admin' && (
242
+ <>
243
+ <select
244
+ className="workspace-role-select"
245
+ value={member.role}
246
+ onChange={(e) => handleUpdateRole(member.userId, e.target.value)}
247
+ >
248
+ <option value="viewer">Viewer</option>
249
+ <option value="editor">Editor</option>
250
+ <option value="admin">Admin</option>
251
+ </select>
252
+ <Button
253
+ variant="ghost"
254
+ size="sm"
255
+ iconOnly
256
+ onClick={() => handleRemoveMember(member.userId)}
257
+ title="Remove member"
258
+ >
259
+ <Trash2 size={13} />
260
+ </Button>
261
+ </>
262
+ )}
263
+ </div>
264
+ </div>
265
+ );
266
+ })}
267
+ </div>
268
+ </section>
269
+
270
+ {/* Pending invites section */}
271
+ {currentUserIsAdmin && (
272
+ <section className="workspace-section">
273
+ <h3 className="workspace-section-title">Pending Invites ({invites.length})</h3>
274
+ {invites.length === 0 ? (
275
+ <p className="workspace-empty">No pending invites</p>
276
+ ) : (
277
+ <div className="workspace-invites-list">
278
+ {invites.map((inv) => (
279
+ <div key={inv.token} className="workspace-invite">
280
+ <span className="workspace-invite-email">{inv.email}</span>
281
+ <span className={cn('workspace-invite-role', ROLE_COLORS[inv.role])}>{inv.role}</span>
282
+ <span className="workspace-invite-expiry">
283
+ Expires {new Date(inv.expiresAt).toLocaleDateString()}
284
+ </span>
285
+ </div>
286
+ ))}
287
+ </div>
288
+ )}
289
+ </section>
290
+ )}
291
+ </div>
292
+ </div>
293
+ );
294
+ }
@@ -0,0 +1,23 @@
1
+ // src/web/views/memory/FromScreenshotPanel.tsx — v5.0.0
2
+ //
3
+ // Memory tab panel that wraps ScreenshotOCR for the three-column Memory layout.
4
+ // Provides a self-contained screenshot → OCR → save workflow.
5
+
6
+ import { ScreenshotOCR } from '../../components/ScreenshotOCR';
7
+ import { Card } from '../../components/Card';
8
+
9
+ type Props = {
10
+ refreshKey: number;
11
+ };
12
+
13
+ export function FromScreenshotPanel({ refreshKey: _refreshKey }: Props) {
14
+ return (
15
+ <div className="memory-panel-content">
16
+ <Card variant="outlined" className="memory-panel-card">
17
+ <div className="memory-panel-body">
18
+ <ScreenshotOCR />
19
+ </div>
20
+ </Card>
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,29 @@
1
+ // src/web/views/memory/MemoryGraphLegend.tsx — color legend for node groups.
2
+
3
+ import React from 'react';
4
+ import { cn } from '../../lib/utils';
5
+
6
+ const LEGEND_ITEMS = [
7
+ { color: '#8b5cf6', label: 'Note / Default' },
8
+ { color: '#34d399', label: 'Entity (LightRAG)' },
9
+ { color: '#fbbf24', label: 'Concept (LightRAG)' },
10
+ { color: '#f87171', label: 'Root / Ungrouped' },
11
+ ];
12
+
13
+ type Props = { className?: string };
14
+
15
+ export function MemoryGraphLegend({ className }: Props) {
16
+ return (
17
+ <div className={cn('memory-graph-legend', className)}>
18
+ {LEGEND_ITEMS.map((item) => (
19
+ <span key={item.label} className="memory-graph-legend-item">
20
+ <span
21
+ className="memory-graph-legend-dot"
22
+ style={{ background: item.color }}
23
+ />
24
+ {item.label}
25
+ </span>
26
+ ))}
27
+ </div>
28
+ );
29
+ }