@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.
- package/bizar-dash/dist/assets/icons-CFqu2M-c.js +656 -0
- package/bizar-dash/dist/assets/icons-CFqu2M-c.js.map +1 -0
- package/bizar-dash/dist/assets/index-DmpSFPJY.js +9 -0
- package/bizar-dash/dist/assets/index-DmpSFPJY.js.map +1 -0
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js +16 -0
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +1 -0
- package/bizar-dash/dist/assets/{main-DX_Jh8Wc.css → main-ZAfGKENE.css} +1 -1
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js +29 -0
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js +2 -0
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-DHXXbn1A.js +1 -0
- package/bizar-dash/dist/assets/{mobile-Chvf9u_B.js.map → mobile-DHXXbn1A.js.map} +1 -1
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js +40 -0
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +1 -0
- package/bizar-dash/dist/index.html +6 -3
- package/bizar-dash/dist/mobile.html +5 -2
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/skills/eval/SKILL.md +237 -0
- package/bizar-dash/src/server/api.mjs +28 -0
- package/bizar-dash/src/server/auth.mjs +155 -1
- package/bizar-dash/src/server/eval-store.mjs +226 -0
- package/bizar-dash/src/server/eval.mjs +347 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +109 -0
- package/bizar-dash/src/server/memory-store.mjs +121 -0
- package/bizar-dash/src/server/ocr.mjs +55 -0
- package/bizar-dash/src/server/otel.mjs +133 -0
- package/bizar-dash/src/server/plugins/registry.mjs +363 -0
- package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
- package/bizar-dash/src/server/plugins/store.mjs +659 -0
- package/bizar-dash/src/server/routes/chat.mjs +246 -170
- package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
- package/bizar-dash/src/server/routes/eval.mjs +147 -0
- package/bizar-dash/src/server/routes/memory.mjs +46 -0
- package/bizar-dash/src/server/routes/ocr.mjs +182 -0
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +82 -48
- package/bizar-dash/src/server/routes/plugins.mjs +220 -0
- package/bizar-dash/src/server/routes/users.mjs +84 -0
- package/bizar-dash/src/server/routes/voice.mjs +131 -0
- package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
- package/bizar-dash/src/server/server.mjs +40 -0
- package/bizar-dash/src/server/voice-store.mjs +202 -0
- package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
- package/bizar-dash/src/server/workspaces.mjs +626 -0
- package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
- package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
- package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +204 -89
- package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
- package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
- package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
- package/bizar-dash/src/web/lib/search.ts +115 -0
- package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +10 -35
- package/bizar-dash/src/web/mobile/views/QrCodePanel.tsx +69 -0
- package/bizar-dash/src/web/styles/memory.css +166 -1
- package/bizar-dash/src/web/styles/settings.css +80 -0
- package/bizar-dash/src/web/views/Memory.tsx +22 -2
- package/bizar-dash/src/web/views/Settings.tsx +99 -0
- package/bizar-dash/src/web/views/Workspace.tsx +294 -0
- package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
- package/bizar-dash/src/web/views/memory/MemoryGraphLegend.tsx +29 -0
- package/bizar-dash/src/web/views/memory/MemoryGraphPanel.tsx +192 -0
- package/bizar-dash/src/web/views/memory/MemoryGraphView.tsx +336 -0
- package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
- package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
- package/bizar-dash/tests/bundle-analysis.test.mjs +73 -0
- package/bizar-dash/tests/clipboard.test.mjs +147 -0
- package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
- package/bizar-dash/tests/components/settings-search.test.tsx +180 -0
- package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
- package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
- package/bizar-dash/tests/docker-build.test.mjs +96 -0
- package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
- package/bizar-dash/tests/eval/report.test.mjs +284 -0
- package/bizar-dash/tests/eval/runner.test.mjs +471 -0
- package/bizar-dash/tests/lib/search-fuzzy.test.ts +149 -0
- package/bizar-dash/tests/memory-graph-view.test.tsx +69 -0
- package/bizar-dash/tests/memory-graph.test.mjs +95 -0
- package/bizar-dash/tests/ocr.test.mjs +87 -0
- package/bizar-dash/tests/otel.test.mjs +188 -0
- package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
- package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
- package/bizar-dash/tests/plugins-store.test.mjs +455 -0
- package/bizar-dash/tests/users.test.mjs +108 -0
- package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
- package/bizar-dash/tests/voice-store.test.mjs +148 -0
- package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
- package/bizar-dash/tests/workspaces.test.mjs +527 -0
- package/cli/bin.mjs +72 -2
- package/cli/commands/clip.mjs +146 -0
- package/cli/commands/dash.mjs +6 -0
- package/cli/commands/deploy/cloudflare.mjs +250 -0
- package/cli/commands/deploy/docker.mjs +221 -0
- package/cli/commands/deploy/fly.mjs +161 -0
- package/cli/commands/deploy/vercel.mjs +225 -0
- package/cli/commands/deploy.mjs +240 -0
- package/cli/commands/eval.mjs +378 -0
- package/cli/commands/marketplace.mjs +64 -0
- package/cli/commands/ocr.mjs +165 -0
- package/cli/commands/plugin.mjs +358 -0
- package/cli/commands/voice.mjs +211 -0
- package/cli/commands/workspace.mjs +247 -0
- package/package.json +12 -2
- package/templates/deploy/cloudflare/README.md +32 -0
- package/templates/deploy/cloudflare/functions-index.template.js +15 -0
- package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
- package/templates/deploy/docker/.env.template +16 -0
- package/templates/deploy/docker/README.md +58 -0
- package/templates/deploy/docker/docker-compose.template.yml +23 -0
- package/templates/deploy/fly/README.md +35 -0
- package/templates/deploy/fly/fly.toml.template +28 -0
- package/templates/deploy/vercel/README.md +29 -0
- package/templates/deploy/vercel/api-index.template.js +18 -0
- package/templates/deploy/vercel/vercel.json.template +16 -0
- package/templates/eval-fixtures/README.md +58 -0
- package/templates/eval-fixtures/code-search-basic.json +28 -0
- package/templates/eval-fixtures/latency-bounds.json +16 -0
- package/templates/eval-fixtures/regression-suite.json +79 -0
- package/templates/eval-fixtures/response-format.json +30 -0
- package/templates/eval-fixtures/tool-call-correctness.json +24 -0
- package/templates/plugin-template/README.md +121 -0
- package/templates/plugin-template/index.js +66 -0
- package/templates/plugin-template/plugin.json +42 -0
- package/templates/plugin-template/tests/plugin.test.js +83 -0
- package/bizar-dash/dist/assets/main-DHZmbnxQ.js +0 -361
- package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js +0 -351
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-Chvf9u_B.js +0 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// src/web/components/InviteDialog.tsx — v5.0.0 — modal to create and manage workspace invites.
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Copy, Check, Trash2, Mail, Link2 } from 'lucide-react';
|
|
4
|
+
import { useModal } from './Modal';
|
|
5
|
+
import { Button } from './Button';
|
|
6
|
+
import { api } from '../lib/api';
|
|
7
|
+
import { cn } from '../lib/utils';
|
|
8
|
+
|
|
9
|
+
export type InviteInfo = {
|
|
10
|
+
token: string;
|
|
11
|
+
email: string;
|
|
12
|
+
role: 'admin' | 'editor' | 'viewer';
|
|
13
|
+
invitedBy: string;
|
|
14
|
+
expiresAt: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type Props = {
|
|
18
|
+
workspaceId: string;
|
|
19
|
+
onInviteCreated?: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function InviteDialog({ workspaceId, onInviteCreated }: Props) {
|
|
23
|
+
const modal = useModal();
|
|
24
|
+
const [email, setEmail] = useState('');
|
|
25
|
+
const [role, setRole] = useState<'admin' | 'editor' | 'viewer'>('editor');
|
|
26
|
+
const [invites, setInvites] = useState<InviteInfo[]>([]);
|
|
27
|
+
const [loading, setLoading] = useState(false);
|
|
28
|
+
const [creating, setCreating] = useState(false);
|
|
29
|
+
const [copied, setCopied] = useState<string | null>(null);
|
|
30
|
+
const [error, setError] = useState<string | null>(null);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
loadInvites();
|
|
34
|
+
}, [workspaceId]);
|
|
35
|
+
|
|
36
|
+
async function loadInvites() {
|
|
37
|
+
setLoading(true);
|
|
38
|
+
try {
|
|
39
|
+
const r = await api.get<{ invites: InviteInfo[] }>(`/workspaces/${workspaceId}/invites`);
|
|
40
|
+
setInvites(r.invites || []);
|
|
41
|
+
} catch {
|
|
42
|
+
// Ignore load errors
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function handleCreate(e: React.FormEvent) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
if (!email.trim()) return;
|
|
51
|
+
setCreating(true);
|
|
52
|
+
setError(null);
|
|
53
|
+
try {
|
|
54
|
+
const result = await api.post<{ token: string; url: string }>(`/workspaces/${workspaceId}/invites`, {
|
|
55
|
+
email: email.trim(),
|
|
56
|
+
role,
|
|
57
|
+
});
|
|
58
|
+
setEmail('');
|
|
59
|
+
await loadInvites();
|
|
60
|
+
onInviteCreated?.();
|
|
61
|
+
|
|
62
|
+
// Show the invite URL in a copy dialog
|
|
63
|
+
modal.open({
|
|
64
|
+
title: 'Invite Link Created',
|
|
65
|
+
children: (
|
|
66
|
+
<div className="invite-link-dialog">
|
|
67
|
+
<p>Share this link with <strong>{email}</strong> to invite them as <strong>{role}</strong>:</p>
|
|
68
|
+
<div className="invite-url-box">
|
|
69
|
+
<Link2 size={14} />
|
|
70
|
+
<input
|
|
71
|
+
type="text"
|
|
72
|
+
readOnly
|
|
73
|
+
value={result.url}
|
|
74
|
+
onClick={(e) => (e.target as HTMLInputElement).select()}
|
|
75
|
+
/>
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
className="invite-copy-btn"
|
|
79
|
+
onClick={() => {
|
|
80
|
+
navigator.clipboard.writeText(result.url).catch(() => undefined);
|
|
81
|
+
setCopied(result.token);
|
|
82
|
+
setTimeout(() => setCopied(null), 2000);
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{copied === result.token ? <Check size={14} /> : <Copy size={14} />}
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
<p className="invite-url-note">This link expires in 7 days.</p>
|
|
89
|
+
</div>
|
|
90
|
+
),
|
|
91
|
+
footer: (
|
|
92
|
+
<Button variant="primary" size="sm" onClick={() => modal.close()}>
|
|
93
|
+
Done
|
|
94
|
+
</Button>
|
|
95
|
+
),
|
|
96
|
+
});
|
|
97
|
+
} catch (err: unknown) {
|
|
98
|
+
setError((err as Error).message || 'Failed to create invite');
|
|
99
|
+
} finally {
|
|
100
|
+
setCreating(false);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function handleRevoke(token: string) {
|
|
105
|
+
if (!confirm('Revoke this invite?')) return;
|
|
106
|
+
try {
|
|
107
|
+
await api.del(`/workspaces/${workspaceId}/invites/${token}`);
|
|
108
|
+
await loadInvites();
|
|
109
|
+
} catch (err: unknown) {
|
|
110
|
+
setError((err as Error).message || 'Failed to revoke invite');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function copyToClipboard(text: string, key: string) {
|
|
115
|
+
navigator.clipboard.writeText(text).catch(() => undefined);
|
|
116
|
+
setCopied(key);
|
|
117
|
+
setTimeout(() => setCopied(null), 2000);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const roleColors: Record<string, string> = {
|
|
121
|
+
admin: 'role-admin',
|
|
122
|
+
editor: 'role-editor',
|
|
123
|
+
viewer: 'role-viewer',
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div className="invite-dialog">
|
|
128
|
+
<form className="invite-form" onSubmit={handleCreate}>
|
|
129
|
+
<div className="invite-form-row">
|
|
130
|
+
<div className="invite-form-field">
|
|
131
|
+
<label htmlFor="invite-email">Email address</label>
|
|
132
|
+
<input
|
|
133
|
+
id="invite-email"
|
|
134
|
+
type="email"
|
|
135
|
+
value={email}
|
|
136
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
137
|
+
placeholder="colleague@example.com"
|
|
138
|
+
required
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="invite-form-field invite-form-field-role">
|
|
142
|
+
<label htmlFor="invite-role">Role</label>
|
|
143
|
+
<select
|
|
144
|
+
id="invite-role"
|
|
145
|
+
value={role}
|
|
146
|
+
onChange={(e) => setRole(e.target.value as 'admin' | 'editor' | 'viewer')}
|
|
147
|
+
>
|
|
148
|
+
<option value="viewer">Viewer</option>
|
|
149
|
+
<option value="editor">Editor</option>
|
|
150
|
+
<option value="admin">Admin</option>
|
|
151
|
+
</select>
|
|
152
|
+
</div>
|
|
153
|
+
<Button
|
|
154
|
+
type="submit"
|
|
155
|
+
variant="primary"
|
|
156
|
+
size="sm"
|
|
157
|
+
loading={creating}
|
|
158
|
+
disabled={!email.trim()}
|
|
159
|
+
>
|
|
160
|
+
<Mail size={13} />
|
|
161
|
+
Invite
|
|
162
|
+
</Button>
|
|
163
|
+
</div>
|
|
164
|
+
{error && <p className="invite-error">{error}</p>}
|
|
165
|
+
</form>
|
|
166
|
+
|
|
167
|
+
<div className="invite-list">
|
|
168
|
+
<h4 className="invite-list-title">Pending Invites</h4>
|
|
169
|
+
{loading && <p className="invite-loading">Loading...</p>}
|
|
170
|
+
{!loading && invites.length === 0 && (
|
|
171
|
+
<p className="invite-empty">No pending invites</p>
|
|
172
|
+
)}
|
|
173
|
+
{invites.map((inv) => (
|
|
174
|
+
<div key={inv.token} className="invite-item">
|
|
175
|
+
<div className="invite-item-info">
|
|
176
|
+
<span className="invite-item-email">{inv.email}</span>
|
|
177
|
+
<span className={cn('invite-item-role', roleColors[inv.role])}>{inv.role}</span>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="invite-item-actions">
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
className="invite-action-btn"
|
|
183
|
+
title="Copy invite link"
|
|
184
|
+
onClick={() => {
|
|
185
|
+
const url = `${window.location.origin}/accept-invite?token=${inv.token}`;
|
|
186
|
+
copyToClipboard(url, inv.token);
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
{copied === inv.token ? <Check size={13} /> : <Copy size={13} />}
|
|
190
|
+
</button>
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
className="invite-action-btn invite-action-revoke"
|
|
194
|
+
title="Revoke invite"
|
|
195
|
+
onClick={() => handleRevoke(inv.token)}
|
|
196
|
+
>
|
|
197
|
+
<Trash2 size={13} />
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
))}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// src/web/components/ScreenshotCapture.tsx — v5.0.0
|
|
2
|
+
//
|
|
3
|
+
// Uses browser's getDisplayMedia() to capture a screen, window, or browser tab.
|
|
4
|
+
// Returns a blob URL for preview and uploads to /api/ocr/process for text extraction.
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback } from 'react';
|
|
7
|
+
import { Camera, Loader2 } from 'lucide-react';
|
|
8
|
+
import { Button } from './Button';
|
|
9
|
+
import { Card, CardTitle } from './Card';
|
|
10
|
+
import { useToast } from './Toast';
|
|
11
|
+
import { api } from '../lib/api';
|
|
12
|
+
|
|
13
|
+
export type ScreenshotCaptureProps = {
|
|
14
|
+
onTextExtracted?: (text: string, notePath: string) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function ScreenshotCapture({ onTextExtracted }: ScreenshotCaptureProps) {
|
|
18
|
+
const toast = useToast();
|
|
19
|
+
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
20
|
+
const [processing, setProcessing] = useState(false);
|
|
21
|
+
const [capturing, setCapturing] = useState(false);
|
|
22
|
+
|
|
23
|
+
const capture = useCallback(async () => {
|
|
24
|
+
setCapturing(true);
|
|
25
|
+
try {
|
|
26
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
27
|
+
video: { mediaSource: 'screen' } as MediaTrackConstraints,
|
|
28
|
+
});
|
|
29
|
+
const video = document.createElement('video');
|
|
30
|
+
video.srcObject = stream;
|
|
31
|
+
await video.play();
|
|
32
|
+
|
|
33
|
+
const canvas = document.createElement('canvas');
|
|
34
|
+
canvas.width = video.videoWidth;
|
|
35
|
+
canvas.height = video.videoHeight;
|
|
36
|
+
const ctx = canvas.getContext('2d');
|
|
37
|
+
if (!ctx) {
|
|
38
|
+
toast.error('Failed to get canvas context');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
ctx.drawImage(video, 0, 0);
|
|
42
|
+
|
|
43
|
+
// Stop all tracks
|
|
44
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
45
|
+
video.remove();
|
|
46
|
+
|
|
47
|
+
canvas.toBlob(async (blob) => {
|
|
48
|
+
if (!blob) {
|
|
49
|
+
toast.error('Failed to capture screenshot');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const url = URL.createObjectURL(blob);
|
|
53
|
+
setImageUrl(url);
|
|
54
|
+
await processImage(blob);
|
|
55
|
+
}, 'image/png');
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if ((err as Error).name !== 'NotAllowedError') {
|
|
58
|
+
toast.error(`Capture failed: ${(err as Error).message}`);
|
|
59
|
+
}
|
|
60
|
+
// NotAllowedError = user cancelled the picker — no toast needed
|
|
61
|
+
} finally {
|
|
62
|
+
setCapturing(false);
|
|
63
|
+
}
|
|
64
|
+
}, [toast]);
|
|
65
|
+
|
|
66
|
+
const processImage = useCallback(async (blob: Blob) => {
|
|
67
|
+
setProcessing(true);
|
|
68
|
+
try {
|
|
69
|
+
const reader = new FileReader();
|
|
70
|
+
const base64 = await new Promise<string>((resolve, reject) => {
|
|
71
|
+
reader.onload = () => {
|
|
72
|
+
const result = reader.result as string;
|
|
73
|
+
// Strip data:image/png;base64, prefix for API
|
|
74
|
+
const b64 = result.includes(',') ? result.split(',')[1] : result;
|
|
75
|
+
resolve(b64);
|
|
76
|
+
};
|
|
77
|
+
reader.onerror = () => reject(new Error('FileReader failed'));
|
|
78
|
+
reader.readAsDataURL(blob);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const r = await api.post<{ ok: boolean; text: string; notePath: string }>('/ocr/process', {
|
|
82
|
+
image: base64,
|
|
83
|
+
lang: 'eng',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (r.ok) {
|
|
87
|
+
toast.success('Text extracted from screenshot');
|
|
88
|
+
onTextExtracted?.(r.text, r.notePath);
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
toast.error(`OCR failed: ${(err as Error).message}`);
|
|
92
|
+
} finally {
|
|
93
|
+
setProcessing(false);
|
|
94
|
+
}
|
|
95
|
+
}, [toast, onTextExtracted]);
|
|
96
|
+
|
|
97
|
+
const dismiss = useCallback(() => {
|
|
98
|
+
setImageUrl(null);
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Card variant="outlined" className="screenshot-capture-card">
|
|
103
|
+
<CardTitle><Camera size={14} /> Screenshot Capture</CardTitle>
|
|
104
|
+
|
|
105
|
+
<div className="screenshot-capture-body">
|
|
106
|
+
{!imageUrl ? (
|
|
107
|
+
<div className="screenshot-capture-empty">
|
|
108
|
+
<Camera size={32} className="muted" />
|
|
109
|
+
<p className="muted text-sm">Capture your screen to extract text via OCR.</p>
|
|
110
|
+
<Button
|
|
111
|
+
variant="primary"
|
|
112
|
+
size="sm"
|
|
113
|
+
onClick={capture}
|
|
114
|
+
disabled={capturing}
|
|
115
|
+
>
|
|
116
|
+
{capturing ? <Loader2 size={12} className="spinner" /> : <Camera size={12} />}
|
|
117
|
+
{capturing ? 'Selecting…' : 'Capture Screen'}
|
|
118
|
+
</Button>
|
|
119
|
+
</div>
|
|
120
|
+
) : (
|
|
121
|
+
<div className="screenshot-capture-preview">
|
|
122
|
+
<img src={imageUrl} alt="Captured screenshot" className="screenshot-capture-img" />
|
|
123
|
+
<div className="screenshot-capture-actions">
|
|
124
|
+
{processing && (
|
|
125
|
+
<span className="text-sm muted">
|
|
126
|
+
<Loader2 size={12} className="spinner" /> Extracting text…
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
<Button variant="ghost" size="sm" onClick={dismiss} disabled={processing}>
|
|
130
|
+
Dismiss
|
|
131
|
+
</Button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</Card>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/web/components/ScreenshotOCR.tsx — v5.0.0
|
|
2
|
+
//
|
|
3
|
+
// Combines ScreenshotCapture + extracted text display in one panel.
|
|
4
|
+
// Shows the screenshot preview and the OCR-extracted text side by side.
|
|
5
|
+
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { FileText } from 'lucide-react';
|
|
8
|
+
import { ScreenshotCapture } from './ScreenshotCapture';
|
|
9
|
+
import { Card, CardTitle, CardMeta } from './Card';
|
|
10
|
+
|
|
11
|
+
export function ScreenshotOCR() {
|
|
12
|
+
const [ocrText, setOcrText] = useState<string | null>(null);
|
|
13
|
+
const [notePath, setNotePath] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
const handleTextExtracted = (text: string, path: string) => {
|
|
16
|
+
setOcrText(text);
|
|
17
|
+
setNotePath(path);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="screenshot-ocr-panel">
|
|
22
|
+
<ScreenshotCapture onTextExtracted={handleTextExtracted} />
|
|
23
|
+
|
|
24
|
+
{ocrText !== null && (
|
|
25
|
+
<Card variant="outlined" className="screenshot-ocr-result-card">
|
|
26
|
+
<CardTitle>
|
|
27
|
+
<FileText size={14} /> Extracted Text
|
|
28
|
+
</CardTitle>
|
|
29
|
+
{notePath && <CardMeta>Saved to: {notePath}</CardMeta>}
|
|
30
|
+
|
|
31
|
+
<div className="screenshot-ocr-text">
|
|
32
|
+
{ocrText ? (
|
|
33
|
+
<pre className="screenshot-ocr-pre mono text-sm">{ocrText}</pre>
|
|
34
|
+
) : (
|
|
35
|
+
<p className="muted text-sm">No text was found in the captured image.</p>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
</Card>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|