mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-19 18:45:15 +02:00
switch to on-demand permission requests and improve suggestion UX
This commit is contained in:
parent
aeb3f13f91
commit
c5aa869adb
12 changed files with 195 additions and 89 deletions
|
|
@ -10,8 +10,26 @@ type PermissionStatus = "authorized" | "denied" | "not determined" | "restricted
|
|||
|
||||
interface PermissionsStatus {
|
||||
accessibility: PermissionStatus;
|
||||
screenRecording: PermissionStatus;
|
||||
}
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
id: "screen-recording",
|
||||
title: "Screen Recording",
|
||||
description: "Lets SurfSense capture your screen to understand context and provide smart writing suggestions.",
|
||||
action: "requestScreenRecording",
|
||||
field: "screenRecording" as const,
|
||||
},
|
||||
{
|
||||
id: "accessibility",
|
||||
title: "Accessibility",
|
||||
description: "Lets SurfSense insert suggestions seamlessly, right where you\u2019re typing.",
|
||||
action: "requestAccessibility",
|
||||
field: "accessibility" as const,
|
||||
},
|
||||
];
|
||||
|
||||
function StatusBadge({ status }: { status: PermissionStatus }) {
|
||||
if (status === "authorized") {
|
||||
return (
|
||||
|
|
@ -48,11 +66,13 @@ export default function DesktopPermissionsPage() {
|
|||
|
||||
let interval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const isResolved = (s: string) => s === "authorized" || s === "restricted";
|
||||
|
||||
const poll = async () => {
|
||||
const status = await window.electronAPI!.getPermissionsStatus();
|
||||
setPermissions(status);
|
||||
|
||||
if (status.accessibility === "authorized" || status.accessibility === "restricted") {
|
||||
if (isResolved(status.accessibility) && isResolved(status.screenRecording)) {
|
||||
if (interval) clearInterval(interval);
|
||||
}
|
||||
};
|
||||
|
|
@ -78,10 +98,14 @@ export default function DesktopPermissionsPage() {
|
|||
);
|
||||
}
|
||||
|
||||
const allGranted = permissions.accessibility === "authorized";
|
||||
const allGranted = permissions.accessibility === "authorized" && permissions.screenRecording === "authorized";
|
||||
|
||||
const handleRequest = async () => {
|
||||
await window.electronAPI!.requestAccessibility();
|
||||
const handleRequest = async (action: string) => {
|
||||
if (action === "requestScreenRecording") {
|
||||
await window.electronAPI!.requestScreenRecording();
|
||||
} else if (action === "requestAccessibility") {
|
||||
await window.electronAPI!.requestAccessibility();
|
||||
}
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
|
|
@ -103,55 +127,61 @@ export default function DesktopPermissionsPage() {
|
|||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">System Permissions</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
SurfSense needs Accessibility permission to insert suggestions into the active application.
|
||||
SurfSense needs two macOS permissions to provide context-aware writing suggestions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Permission card */}
|
||||
{/* Steps */}
|
||||
<div className="rounded-xl border bg-background dark:bg-neutral-900 flex-1 min-h-0 overflow-y-auto px-6 py-6 space-y-6">
|
||||
<div
|
||||
className={`rounded-lg border p-4 transition-colors ${
|
||||
allGranted
|
||||
? "border-green-200 bg-green-50/50 dark:border-green-900 dark:bg-green-950/20"
|
||||
: "border-border"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-medium text-primary">
|
||||
{allGranted ? "\u2713" : "1"}
|
||||
</span>
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium">Accessibility</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Lets SurfSense insert suggestions seamlessly, right where you're typing.
|
||||
</p>
|
||||
{STEPS.map((step, index) => {
|
||||
const status = permissions[step.field];
|
||||
const isGranted = status === "authorized";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
className={`rounded-lg border p-4 transition-colors ${
|
||||
isGranted
|
||||
? "border-green-200 bg-green-50/50 dark:border-green-900 dark:bg-green-950/20"
|
||||
: "border-border"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-medium text-primary">
|
||||
{isGranted ? "\u2713" : index + 1}
|
||||
</span>
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium">{step.title}</h3>
|
||||
<p className="text-xs text-muted-foreground">{step.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={status} />
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={permissions.accessibility} />
|
||||
</div>
|
||||
{!allGranted && (
|
||||
<div className="mt-3 pl-10 space-y-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleRequest}
|
||||
className="text-xs"
|
||||
>
|
||||
Open System Settings
|
||||
</Button>
|
||||
{permissions.accessibility === "denied" && (
|
||||
<p className="text-xs text-amber-700 dark:text-amber-400">
|
||||
Toggle SurfSense on in System Settings to continue.
|
||||
</p>
|
||||
{!isGranted && (
|
||||
<div className="mt-3 pl-10 space-y-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleRequest(step.action)}
|
||||
className="text-xs"
|
||||
>
|
||||
Open System Settings
|
||||
</Button>
|
||||
{status === "denied" && (
|
||||
<p className="text-xs text-amber-700 dark:text-amber-400">
|
||||
Toggle SurfSense on in System Settings to continue.
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
If SurfSense doesn't appear in the list, click <strong>+</strong> and select it from Applications.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
If SurfSense doesn't appear in the list, click <strong>+</strong> and select it from Applications.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
|
|
@ -168,7 +198,7 @@ export default function DesktopPermissionsPage() {
|
|||
) : (
|
||||
<>
|
||||
<Button disabled className="text-sm h-9 min-w-[180px]">
|
||||
Grant permission to continue
|
||||
Grant permissions to continue
|
||||
</Button>
|
||||
<button
|
||||
onClick={handleSkip}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,40 @@ type SSEEvent =
|
|||
| { type: "finish" }
|
||||
| { type: "error"; errorText: string };
|
||||
|
||||
function friendlyError(raw: string | number): string {
|
||||
if (typeof raw === "number") {
|
||||
if (raw === 401) return "Please sign in to use suggestions.";
|
||||
if (raw === 403) return "You don\u2019t have permission for this.";
|
||||
if (raw === 404) return "Suggestion service not found. Is the backend running?";
|
||||
if (raw >= 500) return "Something went wrong on the server. Try again.";
|
||||
return "Something went wrong. Try again.";
|
||||
}
|
||||
const lower = raw.toLowerCase();
|
||||
if (lower.includes("not authenticated") || lower.includes("unauthorized"))
|
||||
return "Please sign in to use suggestions.";
|
||||
if (lower.includes("no vision llm configured") || lower.includes("no llm configured"))
|
||||
return "No Vision LLM configured. Set one in search space settings.";
|
||||
if (lower.includes("fetch") || lower.includes("network") || lower.includes("econnrefused"))
|
||||
return "Can\u2019t reach the server. Check your connection.";
|
||||
return "Something went wrong. Try again.";
|
||||
}
|
||||
|
||||
const AUTO_DISMISS_MS = 3000;
|
||||
|
||||
export default function SuggestionPage() {
|
||||
const [suggestion, setSuggestion] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) return;
|
||||
const timer = setTimeout(() => {
|
||||
window.electronAPI?.dismissSuggestion?.();
|
||||
}, AUTO_DISMISS_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [error]);
|
||||
|
||||
const fetchSuggestion = useCallback(
|
||||
async (screenshot: string, searchSpaceId: string) => {
|
||||
abortRef.current?.abort();
|
||||
|
|
@ -29,7 +57,7 @@ export default function SuggestionPage() {
|
|||
|
||||
const token = getBearerToken();
|
||||
if (!token) {
|
||||
setError("Not authenticated");
|
||||
setError(friendlyError("not authenticated"));
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -55,13 +83,13 @@ export default function SuggestionPage() {
|
|||
);
|
||||
|
||||
if (!response.ok) {
|
||||
setError(`Error: ${response.status}`);
|
||||
setError(friendlyError(response.status));
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
setError("No response body");
|
||||
setError(friendlyError("network error"));
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -94,7 +122,7 @@ export default function SuggestionPage() {
|
|||
return updated;
|
||||
});
|
||||
} else if (parsed.type === "error") {
|
||||
setError(parsed.errorText);
|
||||
setError(friendlyError(parsed.errorText));
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
|
|
@ -104,7 +132,7 @@ export default function SuggestionPage() {
|
|||
}
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") return;
|
||||
setError("Failed to get suggestion");
|
||||
setError(friendlyError("network error"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -145,15 +173,28 @@ export default function SuggestionPage() {
|
|||
);
|
||||
}
|
||||
|
||||
const handleAccept = () => {
|
||||
if (suggestion) {
|
||||
window.electronAPI?.acceptSuggestion?.(suggestion);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
window.electronAPI?.dismissSuggestion?.();
|
||||
};
|
||||
|
||||
if (!suggestion) return null;
|
||||
|
||||
return (
|
||||
<div className="suggestion-tooltip">
|
||||
<p className="suggestion-text">{suggestion}</p>
|
||||
<div className="suggestion-hint">
|
||||
<kbd>Tab</kbd> accept
|
||||
<span className="suggestion-separator" />
|
||||
<kbd>Esc</kbd> dismiss
|
||||
<div className="suggestion-actions">
|
||||
<button className="suggestion-btn suggestion-btn-accept" onClick={handleAccept}>
|
||||
Accept
|
||||
</button>
|
||||
<button className="suggestion-btn suggestion-btn-dismiss" onClick={handleDismiss}>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -36,32 +36,44 @@ html, body {
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.suggestion-hint {
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
.suggestion-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.suggestion-hint kbd {
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #3c3c3c;
|
||||
.suggestion-btn {
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid #3c3c3c;
|
||||
font-family: inherit;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #999;
|
||||
line-height: 18px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
line-height: 16px;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.suggestion-separator {
|
||||
width: 1px;
|
||||
height: 10px;
|
||||
.suggestion-btn-accept {
|
||||
background: #2563eb;
|
||||
border-color: #3b82f6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.suggestion-btn-accept:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.suggestion-btn-dismiss {
|
||||
background: #2a2a2a;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.suggestion-btn-dismiss:hover {
|
||||
background: #333;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.suggestion-error {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue