flawed-avatar 0.2.1
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/assets/animations/idle/Breathing Idle.fbx +0 -0
- package/assets/animations/idle/look away gesture.fbx +0 -0
- package/assets/animations/idle/weight shift.fbx +0 -0
- package/assets/animations/speaking/Agreeing.fbx +0 -0
- package/assets/animations/speaking/Talking (1).fbx +0 -0
- package/assets/animations/speaking/Talking (2).fbx +0 -0
- package/assets/animations/speaking/Talking (3).fbx +0 -0
- package/assets/animations/speaking/Talking.fbx +0 -0
- package/assets/animations/speaking/head nod yes.fbx +0 -0
- package/assets/animations/thinking/Thinking.fbx +0 -0
- package/assets/animations/thinking/thoughtful head shake.fbx +0 -0
- package/assets/animations/working/acknowledging.fbx +0 -0
- package/assets/animations/working/lengthy head nod.fbx +0 -0
- package/assets/icon.png +0 -0
- package/assets/models/CaptainLobster.vrm +0 -0
- package/assets/models/default-avatar.vrm +0 -0
- package/dist/chat-preload.cjs +87 -0
- package/dist/chat-renderer-bundle/chat-index.html +16 -0
- package/dist/chat-renderer-bundle/chat-renderer.js +355 -0
- package/dist/chat-renderer-bundle/styles/base.css +106 -0
- package/dist/chat-renderer-bundle/styles/chat.css +516 -0
- package/dist/chat-renderer-bundle/styles/components/button.css +221 -0
- package/dist/chat-renderer-bundle/styles/components/indicator.css +216 -0
- package/dist/chat-renderer-bundle/styles/components/input.css +139 -0
- package/dist/chat-renderer-bundle/styles/components/toast.css +204 -0
- package/dist/chat-renderer-bundle/styles/controls.css +279 -0
- package/dist/chat-renderer-bundle/styles/settings.css +310 -0
- package/dist/chat-renderer-bundle/styles/tokens.css +220 -0
- package/dist/chat-renderer-bundle/styles/utilities.css +349 -0
- package/dist/main/main/display-utils.d.ts +12 -0
- package/dist/main/main/display-utils.js +29 -0
- package/dist/main/main/gateway-client.d.ts +13 -0
- package/dist/main/main/gateway-client.js +265 -0
- package/dist/main/main/main.d.ts +1 -0
- package/dist/main/main/main.js +157 -0
- package/dist/main/main/persistence/chat-store.d.ts +8 -0
- package/dist/main/main/persistence/chat-store.js +110 -0
- package/dist/main/main/persistence/file-store.d.ts +17 -0
- package/dist/main/main/persistence/file-store.js +183 -0
- package/dist/main/main/persistence/index.d.ts +8 -0
- package/dist/main/main/persistence/index.js +8 -0
- package/dist/main/main/persistence/migrations.d.ts +23 -0
- package/dist/main/main/persistence/migrations.js +191 -0
- package/dist/main/main/persistence/settings-store.d.ts +32 -0
- package/dist/main/main/persistence/settings-store.js +174 -0
- package/dist/main/main/persistence/types.d.ts +72 -0
- package/dist/main/main/persistence/types.js +69 -0
- package/dist/main/main/settings-broadcast.d.ts +3 -0
- package/dist/main/main/settings-broadcast.js +9 -0
- package/dist/main/main/stdin-listener.d.ts +15 -0
- package/dist/main/main/stdin-listener.js +27 -0
- package/dist/main/main/tray.d.ts +3 -0
- package/dist/main/main/tray.js +59 -0
- package/dist/main/main/window-manager.d.ts +23 -0
- package/dist/main/main/window-manager.js +232 -0
- package/dist/main/main/window.d.ts +3 -0
- package/dist/main/main/window.js +528 -0
- package/dist/main/shared/config.d.ts +91 -0
- package/dist/main/shared/config.js +111 -0
- package/dist/main/shared/ipc-channels.d.ts +54 -0
- package/dist/main/shared/ipc-channels.js +68 -0
- package/dist/main/shared/types.d.ts +6 -0
- package/dist/main/shared/types.js +1 -0
- package/dist/preload.cjs +256 -0
- package/dist/renderer-bundle/index.html +63 -0
- package/dist/renderer-bundle/renderer.js +100734 -0
- package/dist/renderer-bundle/styles/base.css +106 -0
- package/dist/renderer-bundle/styles/chat.css +516 -0
- package/dist/renderer-bundle/styles/components/button.css +221 -0
- package/dist/renderer-bundle/styles/components/indicator.css +216 -0
- package/dist/renderer-bundle/styles/components/input.css +139 -0
- package/dist/renderer-bundle/styles/components/toast.css +204 -0
- package/dist/renderer-bundle/styles/controls.css +279 -0
- package/dist/renderer-bundle/styles/settings.css +310 -0
- package/dist/renderer-bundle/styles/tokens.css +220 -0
- package/dist/renderer-bundle/styles/utilities.css +349 -0
- package/index.ts +32 -0
- package/openclaw.plugin.json +22 -0
- package/package.json +45 -0
- package/src/electron-launcher.ts +63 -0
- package/src/main/chat-preload.cjs +87 -0
- package/src/main/display-utils.ts +39 -0
- package/src/main/gateway-client.ts +312 -0
- package/src/main/main.ts +169 -0
- package/src/main/persistence/chat-store.ts +143 -0
- package/src/main/persistence/file-store.ts +221 -0
- package/src/main/persistence/index.ts +69 -0
- package/src/main/persistence/migrations.ts +232 -0
- package/src/main/persistence/settings-store.ts +219 -0
- package/src/main/persistence/types.ts +107 -0
- package/src/main/preload.cjs +256 -0
- package/src/main/settings-broadcast.ts +13 -0
- package/src/main/settings-preload.cjs +153 -0
- package/src/main/stdin-listener.ts +34 -0
- package/src/main/tray.ts +65 -0
- package/src/main/window-manager.ts +298 -0
- package/src/main/window.ts +614 -0
- package/src/renderer/audio/audio-player.ts +161 -0
- package/src/renderer/audio/frequency-analyzer.ts +104 -0
- package/src/renderer/audio/index.ts +36 -0
- package/src/renderer/audio/kokoro-model-loader.ts +128 -0
- package/src/renderer/audio/kokoro-tts-service.ts +370 -0
- package/src/renderer/audio/lip-sync-profile.json +1 -0
- package/src/renderer/audio/phoneme-mapper.ts +120 -0
- package/src/renderer/audio/tts-controller.ts +344 -0
- package/src/renderer/audio/tts-service-factory.ts +75 -0
- package/src/renderer/audio/tts-service.ts +16 -0
- package/src/renderer/audio/types.ts +120 -0
- package/src/renderer/audio/web-speech-tts.ts +177 -0
- package/src/renderer/audio/wlipsync-analyzer.ts +145 -0
- package/src/renderer/avatar/animation-loader.ts +114 -0
- package/src/renderer/avatar/animator.ts +322 -0
- package/src/renderer/avatar/expressions.ts +165 -0
- package/src/renderer/avatar/eye-gaze.ts +255 -0
- package/src/renderer/avatar/eye-saccades.ts +133 -0
- package/src/renderer/avatar/hover-awareness.ts +125 -0
- package/src/renderer/avatar/ibl-enhancer.ts +163 -0
- package/src/renderer/avatar/lip-sync.ts +258 -0
- package/src/renderer/avatar/mixamo-retarget.ts +169 -0
- package/src/renderer/avatar/pixel-transparency.ts +65 -0
- package/src/renderer/avatar/scene.ts +70 -0
- package/src/renderer/avatar/spring-bones.ts +27 -0
- package/src/renderer/avatar/state-machine.ts +117 -0
- package/src/renderer/avatar/vrm-loader.ts +71 -0
- package/src/renderer/chat-window/chat-index.html +16 -0
- package/src/renderer/chat-window/chat-renderer.ts +28 -0
- package/src/renderer/index.html +63 -0
- package/src/renderer/renderer.ts +329 -0
- package/src/renderer/settings-window/settings-controls.ts +223 -0
- package/src/renderer/settings-window/settings-index.html +16 -0
- package/src/renderer/settings-window/settings-panel.ts +346 -0
- package/src/renderer/settings-window/settings-renderer.ts +5 -0
- package/src/renderer/styles/base.css +106 -0
- package/src/renderer/styles/chat.css +516 -0
- package/src/renderer/styles/components/button.css +221 -0
- package/src/renderer/styles/components/indicator.css +216 -0
- package/src/renderer/styles/components/input.css +139 -0
- package/src/renderer/styles/components/toast.css +204 -0
- package/src/renderer/styles/controls.css +279 -0
- package/src/renderer/styles/settings.css +310 -0
- package/src/renderer/styles/tokens.css +220 -0
- package/src/renderer/styles/utilities.css +349 -0
- package/src/renderer/types/avatar-bridge.d.ts +86 -0
- package/src/renderer/types/chat-bridge.d.ts +37 -0
- package/src/renderer/types/settings-bridge.d.ts +54 -0
- package/src/renderer/ui/chat-bubble.ts +435 -0
- package/src/renderer/ui/icons.ts +47 -0
- package/src/renderer/ui/typing-indicator.ts +41 -0
- package/src/service.ts +163 -0
- package/src/shared/config.ts +135 -0
- package/src/shared/ipc-channels.ts +81 -0
- package/src/shared/types.ts +7 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const SETTINGS_SCHEMA_VERSION = 3;
|
|
3
|
+
export declare const CHAT_SCHEMA_VERSION = 1;
|
|
4
|
+
export declare const LightingCustomSchema: z.ZodObject<{
|
|
5
|
+
intensity: z.ZodDefault<z.ZodNumber>;
|
|
6
|
+
color: z.ZodDefault<z.ZodString>;
|
|
7
|
+
ambient: z.ZodDefault<z.ZodNumber>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export type LightingCustom = z.infer<typeof LightingCustomSchema>;
|
|
10
|
+
export declare const SettingsSchema: z.ZodObject<{
|
|
11
|
+
schemaVersion: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
position: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
13
|
+
x: z.ZodNumber;
|
|
14
|
+
y: z.ZodNumber;
|
|
15
|
+
}, z.core.$strip>>>;
|
|
16
|
+
camera: z.ZodOptional<z.ZodObject<{
|
|
17
|
+
zoom: z.ZodNumber;
|
|
18
|
+
}, z.core.$strip>>;
|
|
19
|
+
opacity: z.ZodDefault<z.ZodNumber>;
|
|
20
|
+
idleTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
21
|
+
ttsEnabled: z.ZodDefault<z.ZodBoolean>;
|
|
22
|
+
ttsEngine: z.ZodDefault<z.ZodEnum<{
|
|
23
|
+
"web-speech": "web-speech";
|
|
24
|
+
kokoro: "kokoro";
|
|
25
|
+
}>>;
|
|
26
|
+
ttsVoice: z.ZodDefault<z.ZodString>;
|
|
27
|
+
vrmModelPath: z.ZodOptional<z.ZodString>;
|
|
28
|
+
scale: z.ZodDefault<z.ZodNumber>;
|
|
29
|
+
lightingProfile: z.ZodDefault<z.ZodString>;
|
|
30
|
+
lightingCustom: z.ZodOptional<z.ZodObject<{
|
|
31
|
+
intensity: z.ZodDefault<z.ZodNumber>;
|
|
32
|
+
color: z.ZodDefault<z.ZodString>;
|
|
33
|
+
ambient: z.ZodDefault<z.ZodNumber>;
|
|
34
|
+
}, z.core.$strip>>;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
export type Settings = z.infer<typeof SettingsSchema>;
|
|
37
|
+
export declare const ChatMessageSchema: z.ZodObject<{
|
|
38
|
+
id: z.ZodString;
|
|
39
|
+
timestamp: z.ZodNumber;
|
|
40
|
+
role: z.ZodEnum<{
|
|
41
|
+
user: "user";
|
|
42
|
+
assistant: "assistant";
|
|
43
|
+
}>;
|
|
44
|
+
text: z.ZodString;
|
|
45
|
+
agentId: z.ZodOptional<z.ZodString>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
export declare const ChatHistorySchema: z.ZodObject<{
|
|
48
|
+
schemaVersion: z.ZodDefault<z.ZodNumber>;
|
|
49
|
+
messages: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
50
|
+
id: z.ZodString;
|
|
51
|
+
timestamp: z.ZodNumber;
|
|
52
|
+
role: z.ZodEnum<{
|
|
53
|
+
user: "user";
|
|
54
|
+
assistant: "assistant";
|
|
55
|
+
}>;
|
|
56
|
+
text: z.ZodString;
|
|
57
|
+
agentId: z.ZodOptional<z.ZodString>;
|
|
58
|
+
}, z.core.$strip>>>;
|
|
59
|
+
lastUpdated: z.ZodNumber;
|
|
60
|
+
}, z.core.$strip>;
|
|
61
|
+
export type ChatMessage = z.infer<typeof ChatMessageSchema>;
|
|
62
|
+
export type ChatHistory = z.infer<typeof ChatHistorySchema>;
|
|
63
|
+
export type LoadResult<T> = {
|
|
64
|
+
ok: true;
|
|
65
|
+
data: T;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
error: string;
|
|
69
|
+
fallback: T;
|
|
70
|
+
};
|
|
71
|
+
export declare function createDefaultSettings(): Settings;
|
|
72
|
+
export declare function createDefaultChatHistory(): ChatHistory;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CAMERA_ZOOM_MIN, CAMERA_ZOOM_MAX, CAMERA_ZOOM_DEFAULT, OPACITY_MIN, OPACITY_MAX, OPACITY_DEFAULT, IDLE_TIMEOUT_DEFAULT, CHAT_MAX_HISTORY, TTS_ENABLED_DEFAULT, TTS_ENGINE_DEFAULT, TTS_VOICE_DEFAULT, SCALE_MIN, SCALE_MAX, SCALE_DEFAULT, LIGHTING_PROFILE_DEFAULT, } from "../../shared/config.js";
|
|
3
|
+
export const SETTINGS_SCHEMA_VERSION = 3;
|
|
4
|
+
export const CHAT_SCHEMA_VERSION = 1;
|
|
5
|
+
// === Lighting Custom Schema ===
|
|
6
|
+
export const LightingCustomSchema = z.object({
|
|
7
|
+
intensity: z.number().min(0).max(2).default(0.3),
|
|
8
|
+
color: z.string().default("#ffffff"),
|
|
9
|
+
ambient: z.number().min(0).max(1).default(0.5),
|
|
10
|
+
});
|
|
11
|
+
// === Settings Schema ===
|
|
12
|
+
export const SettingsSchema = z.object({
|
|
13
|
+
schemaVersion: z.number().default(SETTINGS_SCHEMA_VERSION),
|
|
14
|
+
position: z
|
|
15
|
+
.record(z.string(), z.object({
|
|
16
|
+
x: z.number().finite(),
|
|
17
|
+
y: z.number().finite(),
|
|
18
|
+
}))
|
|
19
|
+
.optional(),
|
|
20
|
+
camera: z
|
|
21
|
+
.object({
|
|
22
|
+
zoom: z.number().finite().min(CAMERA_ZOOM_MIN).max(CAMERA_ZOOM_MAX),
|
|
23
|
+
})
|
|
24
|
+
.optional(),
|
|
25
|
+
opacity: z.number().min(OPACITY_MIN).max(OPACITY_MAX).default(OPACITY_DEFAULT),
|
|
26
|
+
idleTimeoutMs: z.number().int().min(0).default(IDLE_TIMEOUT_DEFAULT),
|
|
27
|
+
ttsEnabled: z.boolean().default(TTS_ENABLED_DEFAULT),
|
|
28
|
+
ttsEngine: z.enum(["web-speech", "kokoro"]).default(TTS_ENGINE_DEFAULT),
|
|
29
|
+
ttsVoice: z.string().default(TTS_VOICE_DEFAULT),
|
|
30
|
+
vrmModelPath: z.string().optional(),
|
|
31
|
+
scale: z.number().min(SCALE_MIN).max(SCALE_MAX).default(SCALE_DEFAULT),
|
|
32
|
+
lightingProfile: z.string().default(LIGHTING_PROFILE_DEFAULT),
|
|
33
|
+
lightingCustom: LightingCustomSchema.optional(),
|
|
34
|
+
});
|
|
35
|
+
// === Chat Message Schema ===
|
|
36
|
+
export const ChatMessageSchema = z.object({
|
|
37
|
+
id: z.string(),
|
|
38
|
+
timestamp: z.number().int().positive(),
|
|
39
|
+
role: z.enum(["user", "assistant"]),
|
|
40
|
+
text: z.string().max(10000),
|
|
41
|
+
agentId: z.string().optional(),
|
|
42
|
+
});
|
|
43
|
+
export const ChatHistorySchema = z.object({
|
|
44
|
+
schemaVersion: z.number().default(CHAT_SCHEMA_VERSION),
|
|
45
|
+
messages: z.array(ChatMessageSchema).max(CHAT_MAX_HISTORY).default([]),
|
|
46
|
+
lastUpdated: z.number().int().positive(),
|
|
47
|
+
});
|
|
48
|
+
// === Default Factories ===
|
|
49
|
+
export function createDefaultSettings() {
|
|
50
|
+
return {
|
|
51
|
+
schemaVersion: SETTINGS_SCHEMA_VERSION,
|
|
52
|
+
position: {},
|
|
53
|
+
camera: { zoom: CAMERA_ZOOM_DEFAULT },
|
|
54
|
+
opacity: OPACITY_DEFAULT,
|
|
55
|
+
idleTimeoutMs: IDLE_TIMEOUT_DEFAULT,
|
|
56
|
+
ttsEnabled: TTS_ENABLED_DEFAULT,
|
|
57
|
+
ttsEngine: TTS_ENGINE_DEFAULT,
|
|
58
|
+
ttsVoice: TTS_VOICE_DEFAULT,
|
|
59
|
+
scale: SCALE_DEFAULT,
|
|
60
|
+
lightingProfile: LIGHTING_PROFILE_DEFAULT,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function createDefaultChatHistory() {
|
|
64
|
+
return {
|
|
65
|
+
schemaVersion: CHAT_SCHEMA_VERSION,
|
|
66
|
+
messages: [],
|
|
67
|
+
lastUpdated: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type StdinCommand = {
|
|
2
|
+
type: "show";
|
|
3
|
+
} | {
|
|
4
|
+
type: "hide";
|
|
5
|
+
} | {
|
|
6
|
+
type: "shutdown";
|
|
7
|
+
} | {
|
|
8
|
+
type: "model-switch";
|
|
9
|
+
vrmPath: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Listen for newline-delimited JSON commands on stdin.
|
|
13
|
+
* Returns a cleanup function to stop listening.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createStdinListener(handler: (cmd: StdinCommand) => void): () => void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
/**
|
|
3
|
+
* Listen for newline-delimited JSON commands on stdin.
|
|
4
|
+
* Returns a cleanup function to stop listening.
|
|
5
|
+
*/
|
|
6
|
+
export function createStdinListener(handler) {
|
|
7
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
8
|
+
rl.on("line", (line) => {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
if (!trimmed)
|
|
11
|
+
return;
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(trimmed);
|
|
14
|
+
if (typeof parsed?.type === "string") {
|
|
15
|
+
if (parsed.type === "model-switch" && typeof parsed.vrmPath !== "string")
|
|
16
|
+
return;
|
|
17
|
+
handler(parsed);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Ignore malformed lines
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return () => {
|
|
25
|
+
rl.close();
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Tray, Menu, app } from "electron";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { showVrmPicker } from "./window.js";
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
let tray = null;
|
|
8
|
+
export function createTray(wm) {
|
|
9
|
+
const iconPath = path.join(__dirname, "..", "..", "..", "assets", "icon.png");
|
|
10
|
+
tray = new Tray(iconPath);
|
|
11
|
+
tray.setToolTip("Flawed Avatar");
|
|
12
|
+
function rebuildMenu() {
|
|
13
|
+
const menu = Menu.buildFromTemplate([
|
|
14
|
+
{
|
|
15
|
+
label: wm.avatarWin.isVisible() ? "Hide Avatar" : "Show Avatar",
|
|
16
|
+
click() {
|
|
17
|
+
if (wm.avatarWin.isVisible()) {
|
|
18
|
+
wm.hideAll();
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
wm.showAvatar();
|
|
22
|
+
}
|
|
23
|
+
rebuildMenu();
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: "Change Avatar Model\u2026",
|
|
28
|
+
click() {
|
|
29
|
+
showVrmPicker(wm.avatarWin);
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
label: "Settings\u2026",
|
|
34
|
+
click() {
|
|
35
|
+
wm.showSettings();
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{ type: "separator" },
|
|
39
|
+
{
|
|
40
|
+
label: "Quit",
|
|
41
|
+
click() {
|
|
42
|
+
app.quit();
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
tray.setContextMenu(menu);
|
|
47
|
+
}
|
|
48
|
+
rebuildMenu();
|
|
49
|
+
tray.on("click", () => {
|
|
50
|
+
if (wm.avatarWin.isVisible()) {
|
|
51
|
+
wm.hideAll();
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
wm.showAvatar();
|
|
55
|
+
}
|
|
56
|
+
rebuildMenu();
|
|
57
|
+
});
|
|
58
|
+
return tray;
|
|
59
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BrowserWindow } from "electron";
|
|
2
|
+
import type { AgentState } from "../shared/types.js";
|
|
3
|
+
export interface WindowManager {
|
|
4
|
+
avatarWin: BrowserWindow;
|
|
5
|
+
chatWin: BrowserWindow;
|
|
6
|
+
settingsWin: BrowserWindow | null;
|
|
7
|
+
chatVisible: boolean;
|
|
8
|
+
toggleChat(): void;
|
|
9
|
+
showChat(): void;
|
|
10
|
+
hideChat(): void;
|
|
11
|
+
repositionChat(): void;
|
|
12
|
+
showSettings(): void;
|
|
13
|
+
hideSettings(): void;
|
|
14
|
+
toggleSettings(): void;
|
|
15
|
+
hideAll(): void;
|
|
16
|
+
showAvatar(): void;
|
|
17
|
+
sendAgentState(state: AgentState): void;
|
|
18
|
+
sendToAvatar(channel: string, ...args: unknown[]): void;
|
|
19
|
+
sendToChat(channel: string, ...args: unknown[]): void;
|
|
20
|
+
sendToSettings(channel: string, ...args: unknown[]): void;
|
|
21
|
+
destroyAll(): void;
|
|
22
|
+
}
|
|
23
|
+
export declare function createWindowManager(): WindowManager;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { BrowserWindow, ipcMain, screen } from "electron";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { createOverlayWindow } from "./window.js";
|
|
5
|
+
import { IPC } from "../shared/ipc-channels.js";
|
|
6
|
+
import { WINDOW_HEIGHT, CHAT_WINDOW_WIDTH, CHAT_WINDOW_HEIGHT, CHAT_WINDOW_GAP, SETTINGS_WINDOW_WIDTH, SETTINGS_WINDOW_HEIGHT, } from "../shared/config.js";
|
|
7
|
+
import { clampBoundsToWorkArea } from "./display-utils.js";
|
|
8
|
+
import { setSettingsBroadcastTarget } from "./settings-broadcast.js";
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
function computeChatPosition(avatarX, avatarY, avatarWidth) {
|
|
12
|
+
const point = { x: avatarX + avatarWidth / 2, y: avatarY };
|
|
13
|
+
const display = screen.getDisplayNearestPoint(point);
|
|
14
|
+
const workArea = display.workArea;
|
|
15
|
+
const chatX = Math.round(avatarX + avatarWidth / 2 - CHAT_WINDOW_WIDTH / 2);
|
|
16
|
+
// Try above avatar first
|
|
17
|
+
const aboveY = avatarY - CHAT_WINDOW_HEIGHT - CHAT_WINDOW_GAP;
|
|
18
|
+
if (aboveY >= workArea.y) {
|
|
19
|
+
return clampBoundsToWorkArea(chatX, aboveY, CHAT_WINDOW_WIDTH, CHAT_WINDOW_HEIGHT);
|
|
20
|
+
}
|
|
21
|
+
// Fall back to below avatar
|
|
22
|
+
const belowY = avatarY + WINDOW_HEIGHT + CHAT_WINDOW_GAP;
|
|
23
|
+
return clampBoundsToWorkArea(chatX, belowY, CHAT_WINDOW_WIDTH, CHAT_WINDOW_HEIGHT);
|
|
24
|
+
}
|
|
25
|
+
function createChatWindow() {
|
|
26
|
+
const chatWin = new BrowserWindow({
|
|
27
|
+
width: CHAT_WINDOW_WIDTH,
|
|
28
|
+
height: CHAT_WINDOW_HEIGHT,
|
|
29
|
+
transparent: true,
|
|
30
|
+
frame: false,
|
|
31
|
+
alwaysOnTop: true,
|
|
32
|
+
skipTaskbar: true,
|
|
33
|
+
resizable: false,
|
|
34
|
+
hasShadow: false,
|
|
35
|
+
show: false,
|
|
36
|
+
webPreferences: {
|
|
37
|
+
contextIsolation: true,
|
|
38
|
+
nodeIntegration: false,
|
|
39
|
+
preload: path.join(__dirname, "..", "..", "chat-preload.cjs"),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
chatWin.loadFile(path.join(__dirname, "..", "..", "chat-renderer-bundle", "chat-index.html"));
|
|
43
|
+
// Chat window should be interactive by default (not click-through)
|
|
44
|
+
// Only the transparent areas around the chat body will pass clicks through
|
|
45
|
+
return chatWin;
|
|
46
|
+
}
|
|
47
|
+
function createSettingsWindow() {
|
|
48
|
+
const settingsWin = new BrowserWindow({
|
|
49
|
+
width: SETTINGS_WINDOW_WIDTH,
|
|
50
|
+
height: SETTINGS_WINDOW_HEIGHT,
|
|
51
|
+
transparent: false,
|
|
52
|
+
frame: false,
|
|
53
|
+
alwaysOnTop: false,
|
|
54
|
+
skipTaskbar: false,
|
|
55
|
+
resizable: true,
|
|
56
|
+
minWidth: 320,
|
|
57
|
+
minHeight: 400,
|
|
58
|
+
show: false,
|
|
59
|
+
webPreferences: {
|
|
60
|
+
contextIsolation: true,
|
|
61
|
+
nodeIntegration: false,
|
|
62
|
+
preload: path.join(__dirname, "..", "..", "settings-preload.cjs"),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
settingsWin.loadFile(path.join(__dirname, "..", "..", "settings-renderer-bundle", "settings-index.html"));
|
|
66
|
+
return settingsWin;
|
|
67
|
+
}
|
|
68
|
+
export function createWindowManager() {
|
|
69
|
+
const avatarWin = createOverlayWindow();
|
|
70
|
+
let chatWin = createChatWindow();
|
|
71
|
+
let chatVisible = false;
|
|
72
|
+
let settingsWin = null;
|
|
73
|
+
// Handle chat window being closed externally
|
|
74
|
+
chatWin.on("closed", () => {
|
|
75
|
+
chatVisible = false;
|
|
76
|
+
chatWin = createChatWindow();
|
|
77
|
+
});
|
|
78
|
+
function repositionChat() {
|
|
79
|
+
if (chatWin.isDestroyed())
|
|
80
|
+
return;
|
|
81
|
+
const [ax, ay] = avatarWin.getPosition();
|
|
82
|
+
const [aw] = avatarWin.getSize();
|
|
83
|
+
const pos = computeChatPosition(ax, ay, aw);
|
|
84
|
+
chatWin.setPosition(pos.x, pos.y);
|
|
85
|
+
}
|
|
86
|
+
function showChat() {
|
|
87
|
+
if (chatVisible)
|
|
88
|
+
return;
|
|
89
|
+
chatVisible = true;
|
|
90
|
+
repositionChat();
|
|
91
|
+
chatWin.showInactive();
|
|
92
|
+
chatWin.webContents.send(IPC.SHOW_CHAT_BUBBLE);
|
|
93
|
+
avatarWin.webContents.send(IPC.CHAT_VISIBILITY, true);
|
|
94
|
+
}
|
|
95
|
+
function hideChat() {
|
|
96
|
+
if (!chatVisible)
|
|
97
|
+
return;
|
|
98
|
+
chatVisible = false;
|
|
99
|
+
if (!chatWin.isDestroyed())
|
|
100
|
+
chatWin.hide();
|
|
101
|
+
avatarWin.webContents.send(IPC.CHAT_VISIBILITY, false);
|
|
102
|
+
}
|
|
103
|
+
function toggleChat() {
|
|
104
|
+
if (chatVisible) {
|
|
105
|
+
hideChat();
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
showChat();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function showSettings() {
|
|
112
|
+
if (avatarWin.isDestroyed())
|
|
113
|
+
return;
|
|
114
|
+
if (!settingsWin || settingsWin.isDestroyed()) {
|
|
115
|
+
settingsWin = createSettingsWindow();
|
|
116
|
+
setSettingsBroadcastTarget(settingsWin);
|
|
117
|
+
settingsWin.on("closed", () => {
|
|
118
|
+
settingsWin = null;
|
|
119
|
+
setSettingsBroadcastTarget(null);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const [ax, ay] = avatarWin.getPosition();
|
|
123
|
+
const pos = clampBoundsToWorkArea(ax - SETTINGS_WINDOW_WIDTH - 12, ay, SETTINGS_WINDOW_WIDTH, SETTINGS_WINDOW_HEIGHT);
|
|
124
|
+
settingsWin.setPosition(pos.x, pos.y);
|
|
125
|
+
settingsWin.show();
|
|
126
|
+
settingsWin.focus();
|
|
127
|
+
}
|
|
128
|
+
function hideSettings() {
|
|
129
|
+
if (settingsWin && !settingsWin.isDestroyed())
|
|
130
|
+
settingsWin.hide();
|
|
131
|
+
}
|
|
132
|
+
function toggleSettings() {
|
|
133
|
+
if (settingsWin && !settingsWin.isDestroyed() && settingsWin.isVisible()) {
|
|
134
|
+
hideSettings();
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
showSettings();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function sendToSettings(channel, ...args) {
|
|
141
|
+
if (settingsWin && !settingsWin.isDestroyed()) {
|
|
142
|
+
settingsWin.webContents.send(channel, ...args);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function hideAll() {
|
|
146
|
+
hideChat();
|
|
147
|
+
hideSettings();
|
|
148
|
+
avatarWin.hide();
|
|
149
|
+
}
|
|
150
|
+
function showAvatar() {
|
|
151
|
+
avatarWin.show();
|
|
152
|
+
}
|
|
153
|
+
function sendAgentState(state) {
|
|
154
|
+
avatarWin.webContents.send(IPC.AGENT_STATE, state);
|
|
155
|
+
if (!chatWin.isDestroyed()) {
|
|
156
|
+
chatWin.webContents.send(IPC.AGENT_STATE, state);
|
|
157
|
+
}
|
|
158
|
+
// Auto-show chat on non-idle states
|
|
159
|
+
if (state.phase !== "idle") {
|
|
160
|
+
showChat();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function sendToAvatar(channel, ...args) {
|
|
164
|
+
if (!avatarWin.isDestroyed()) {
|
|
165
|
+
avatarWin.webContents.send(channel, ...args);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function sendToChat(channel, ...args) {
|
|
169
|
+
if (!chatWin.isDestroyed()) {
|
|
170
|
+
chatWin.webContents.send(channel, ...args);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function destroyAll() {
|
|
174
|
+
if (settingsWin && !settingsWin.isDestroyed())
|
|
175
|
+
settingsWin.destroy();
|
|
176
|
+
if (!chatWin.isDestroyed())
|
|
177
|
+
chatWin.destroy();
|
|
178
|
+
if (!avatarWin.isDestroyed())
|
|
179
|
+
avatarWin.destroy();
|
|
180
|
+
}
|
|
181
|
+
// Reposition chat when avatar moves
|
|
182
|
+
avatarWin.on("moved", () => {
|
|
183
|
+
if (chatVisible)
|
|
184
|
+
repositionChat();
|
|
185
|
+
});
|
|
186
|
+
// Toggle chat from avatar renderer
|
|
187
|
+
ipcMain.on(IPC.TOGGLE_CHAT, () => {
|
|
188
|
+
toggleChat();
|
|
189
|
+
});
|
|
190
|
+
// Chat window click-through toggle
|
|
191
|
+
ipcMain.on(IPC.SET_IGNORE_MOUSE_CHAT, (_event, ignore) => {
|
|
192
|
+
if (typeof ignore !== "boolean" || chatWin.isDestroyed())
|
|
193
|
+
return;
|
|
194
|
+
chatWin.setIgnoreMouseEvents(ignore, { forward: true });
|
|
195
|
+
});
|
|
196
|
+
// Chat content hidden (idle fade) → hide the BrowserWindow
|
|
197
|
+
ipcMain.on(IPC.CHAT_CONTENT_HIDDEN, () => {
|
|
198
|
+
hideChat();
|
|
199
|
+
});
|
|
200
|
+
// Chat content shown → ensure window is visible
|
|
201
|
+
ipcMain.on(IPC.CHAT_CONTENT_SHOWN, () => {
|
|
202
|
+
if (!chatVisible)
|
|
203
|
+
showChat();
|
|
204
|
+
});
|
|
205
|
+
// Settings window open/close from renderer
|
|
206
|
+
ipcMain.on(IPC.OPEN_SETTINGS, () => {
|
|
207
|
+
showSettings();
|
|
208
|
+
});
|
|
209
|
+
ipcMain.on(IPC.CLOSE_SETTINGS, () => {
|
|
210
|
+
hideSettings();
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
get avatarWin() { return avatarWin; },
|
|
214
|
+
get chatWin() { return chatWin; },
|
|
215
|
+
get settingsWin() { return settingsWin; },
|
|
216
|
+
get chatVisible() { return chatVisible; },
|
|
217
|
+
toggleChat,
|
|
218
|
+
showChat,
|
|
219
|
+
hideChat,
|
|
220
|
+
repositionChat,
|
|
221
|
+
showSettings,
|
|
222
|
+
hideSettings,
|
|
223
|
+
toggleSettings,
|
|
224
|
+
hideAll,
|
|
225
|
+
showAvatar,
|
|
226
|
+
sendAgentState,
|
|
227
|
+
sendToAvatar,
|
|
228
|
+
sendToChat,
|
|
229
|
+
sendToSettings,
|
|
230
|
+
destroyAll,
|
|
231
|
+
};
|
|
232
|
+
}
|