mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-18 20:14:44 +08:00
feat: add notification
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: "相关的致谢信息会展示在这里。",
|
||||
|
||||
99
frontend/src/core/notification/hooks.ts
Normal file
99
frontend/src/core/notification/hooks.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user