feat: add notification

This commit is contained in:
Henry Li
2026-01-31 11:08:27 +08:00
parent 4e0571f3b3
commit c62caf95c4
18 changed files with 482 additions and 56 deletions

View File

@@ -11,7 +11,11 @@ export async function loadArtifactContent({
filepath: string;
threadId: string;
}) {
const url = urlOfArtifact({ filepath, threadId });
let enhancedFilepath = filepath;
if (filepath.endsWith(".skill")) {
enhancedFilepath = filepath.replace(".md", ".skill/SKILL.md");
}
const url = urlOfArtifact({ filepath: enhancedFilepath, threadId });
const response = await fetch(url);
const text = await response.text();
return text;

View File

@@ -28,6 +28,7 @@ export const enUS: Translations = {
preview: "Preview",
cancel: "Cancel",
save: "Save",
install: "Install",
},
// Welcome
@@ -125,6 +126,7 @@ export const enUS: Translations = {
appearance: "Appearance",
tools: "Tools",
skills: "Skills",
notification: "Notification",
acknowledge: "Acknowledge",
},
appearance: {
@@ -149,6 +151,19 @@ export const enUS: Translations = {
description:
"Manage the configuration and enabled status of the agent skills.",
},
notification: {
title: "Notification",
description:
"DeerFlow only sends a completion notification when the window is not active. This is especially useful for long-running tasks so you can switch to other work and get notified when done.",
requestPermission: "Request notification permission",
deniedHint:
"Notification permission was denied. You can enable it in your browser's site settings to receive completion alerts.",
testButton: "Send test notification",
testTitle: "DeerFlow",
testBody: "This is a test notification.",
notSupported: "Your browser does not support notifications.",
disableNotification: "Disable notification",
},
acknowledge: {
emptyTitle: "Acknowledgements",
emptyDescription: "Credits and acknowledgements will show here.",

View File

@@ -26,6 +26,7 @@ export interface Translations {
preview: string;
cancel: string;
save: string;
install: string;
};
// Welcome
@@ -119,6 +120,7 @@ export interface Translations {
appearance: string;
tools: string;
skills: string;
notification: string;
acknowledge: string;
};
appearance: {
@@ -141,6 +143,17 @@ export interface Translations {
title: string;
description: string;
};
notification: {
title: string;
description: string;
requestPermission: string;
deniedHint: string;
testButton: string;
testTitle: string;
testBody: string;
notSupported: string;
disableNotification: string;
};
acknowledge: {
emptyTitle: string;
emptyDescription: string;

View File

@@ -28,6 +28,7 @@ export const zhCN: Translations = {
preview: "预览",
cancel: "取消",
save: "保存",
install: "安装",
},
// Welcome
@@ -122,6 +123,7 @@ export const zhCN: Translations = {
appearance: "外观",
tools: "工具",
skills: "技能",
notification: "通知",
acknowledge: "致谢",
},
appearance: {
@@ -144,6 +146,19 @@ export const zhCN: Translations = {
title: "技能",
description: "管理 Agent Skill 配置和启用状态。",
},
notification: {
title: "通知",
description:
"DeerFlow 只会在窗口不活跃时发送完成通知,特别适合长时间任务:你可以先去做别的事,完成后会收到提醒。",
requestPermission: "请求通知权限",
deniedHint:
"通知权限已被拒绝。可在浏览器的网站设置中重新开启,以接收完成提醒。",
testButton: "发送测试通知",
testTitle: "DeerFlow",
testBody: "这是一条测试通知。",
notSupported: "当前浏览器不支持通知功能。",
disableNotification: "关闭通知",
},
acknowledge: {
emptyTitle: "致谢",
emptyDescription: "相关的致谢信息会展示在这里。",

View File

@@ -0,0 +1,99 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useLocalSettings } from "../settings";
interface NotificationOptions {
body?: string;
icon?: string;
badge?: string;
tag?: string;
data?: unknown;
requireInteraction?: boolean;
silent?: boolean;
}
interface UseNotificationReturn {
permission: NotificationPermission;
isSupported: boolean;
requestPermission: () => Promise<NotificationPermission>;
showNotification: (title: string, options?: NotificationOptions) => void;
}
export function useNotification(): UseNotificationReturn {
const [permission, setPermission] =
useState<NotificationPermission>("default");
const [isSupported, setIsSupported] = useState(false);
const lastNotificationTime = useRef<Date>(new Date());
useEffect(() => {
// Check if browser supports Notification API
if ("Notification" in window) {
setIsSupported(true);
setPermission(Notification.permission);
}
}, []);
const requestPermission =
useCallback(async (): Promise<NotificationPermission> => {
if (!isSupported) {
console.warn("Notification API is not supported in this browser");
return "denied";
}
const result = await Notification.requestPermission();
setPermission(result);
return result;
}, [isSupported]);
const [settings] = useLocalSettings();
const showNotification = useCallback(
(title: string, options?: NotificationOptions) => {
if (!isSupported) {
console.warn("Notification API is not supported");
return;
}
if (!settings.notification.enabled) {
console.warn("Notification is disabled");
return;
}
if (
new Date().getTime() - lastNotificationTime.current.getTime() <
1000
) {
console.warn("Notification sent too soon");
return;
}
lastNotificationTime.current = new Date();
if (permission !== "granted") {
console.warn("Notification permission not granted");
return;
}
const notification = new Notification(title, options);
// Optional: Add event listeners
notification.onclick = () => {
console.log("Notification clicked");
window.focus();
notification.close();
};
notification.onerror = (error) => {
console.error("Notification error:", error);
};
},
[isSupported, settings.notification.enabled, permission],
);
return {
permission,
isSupported,
requestPermission,
showNotification,
};
}

View File

@@ -1,6 +1,9 @@
import type { AgentThreadContext } from "../threads";
export const DEFAULT_LOCAL_SETTINGS: LocalSettings = {
notification: {
enabled: true,
},
context: {
model_name: undefined,
mode: undefined,
@@ -13,6 +16,9 @@ export const DEFAULT_LOCAL_SETTINGS: LocalSettings = {
const LOCAL_SETTINGS_KEY = "deerflow.local-settings";
export interface LocalSettings {
notification: {
enabled: boolean;
};
context: Omit<
AgentThreadContext,
"thread_id" | "is_plan_mode" | "thinking_enabled"
@@ -42,6 +48,10 @@ export function getLocalSettings(): LocalSettings {
...DEFAULT_LOCAL_SETTINGS.layout,
...settings.layout,
},
notification: {
...DEFAULT_LOCAL_SETTINGS.notification,
...settings.notification,
},
};
return mergedSettings;
}

View File

@@ -23,3 +23,13 @@ export async function enableSkill(skillName: string, enabled: boolean) {
);
return response.json();
}
export async function installSkill(skillName: string) {
const response = await fetch(
`${getBackendBaseURL()}/api/skills/${skillName}/install`,
{
method: "POST",
},
);
return response.json();
}

View File

@@ -18,9 +18,11 @@ import type {
export function useThreadStream({
threadId,
isNewThread,
onFinish,
}: {
isNewThread: boolean;
threadId: string | null | undefined;
onFinish?: (state: AgentThreadState) => void;
}) {
const queryClient = useQueryClient();
const thread = useStream<AgentThreadState>({
@@ -30,6 +32,7 @@ export function useThreadStream({
reconnectOnMount: true,
fetchStateHistory: true,
onFinish(state) {
onFinish?.(state.values);
// void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
queryClient.setQueriesData(
{