Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions e2e/verify_tip_proxy.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { test, expect } from '@playwright/test';

test('launch tip-of-the-day proxy', async ({ page }) => {
await page.goto('/win98-web/');

// Wait for system to boot and Clippy to be available
await page.waitForSelector('.start-button');

// Launch Tip of the Day via console to trigger proxy
// Use a try-catch and wait for System.launchApp to be available if needed
await page.evaluate(async () => {
const waitForSystem = () => new Promise(resolve => {
if (window.System && typeof window.System.launchApp === 'function') {
resolve();
} else {
const interval = setInterval(() => {
if (window.System && typeof window.System.launchApp === 'function') {
clearInterval(interval);
resolve();
}
}, 100);
}
});
await waitForSystem();
await window.System.launchApp('tip-of-the-day');
});

// Verify Assistant (Clippy) appears with a tip
// Clippy takes a moment to load and show the balloon
const balloon = page.locator('.clippy-balloon');
await expect(balloon).toBeVisible({ timeout: 15000 });

const tipText = await balloon.innerText();
console.log('Tip shown by Assistant:', tipText);
expect(tipText.length).toBeGreaterThan(0);
});
4 changes: 2 additions & 2 deletions src/apps/clippy/clippy-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export class ClippyApp extends Application {
return null;
}

_onLaunch() {
_onLaunch(data) {
// Call the legacy launch function.
launchClippyApp(this);
launchClippyApp(this, undefined, { showTip: data?.showTip });
}

_cleanup() {
Expand Down
62 changes: 56 additions & 6 deletions src/apps/clippy/clippy.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
releaseBusyState,
} from '../../system/busy-state-manager.js';
import { appManager } from '../../system/app-manager.js';
import { apps } from '../../config/apps.js';
import { getStartupApps, addStartupApp, removeStartupApp } from '../../system/startup-manager.js';

window.clippyAppInstance = null;
let currentAgentName =
Expand Down Expand Up @@ -60,6 +62,26 @@ async function askClippy(agent, question) {
}
}

async function showRandomTip(agent) {
const tips = apps.reduce((acc, app) => {
if (app.tips) {
return acc.concat(app.tips);
}
return acc;
}, []);

if (tips.length === 0) return;

const currentTipIndex = Math.floor(Math.random() * tips.length);
const tip = tips[currentTipIndex];
const ttsEnabled = agent.isTTSEnabled();

// Create a temporary element to strip HTML for TTS if needed,
// though speakAndAnimate might handle it.
// The tips might contain <a> tags.
await agent.speakAndAnimate(tip, "Explain", { useTTS: ttsEnabled });
}

import { AGENT_NAMES } from '../../config/agents.js';

export function getClippyMenuItems(app) {
Expand All @@ -76,6 +98,10 @@ export function getClippyMenuItems(app) {
label: "&Animate",
action: () => agent.animate(),
},
{
label: "&Show Tip",
action: () => showRandomTip(agent),
},
{
label: "&Ask Clippy",
default: true,
Expand All @@ -100,6 +126,25 @@ export function getClippyMenuItems(app) {
},
},
},
{
label: "Show tips at startup",
checkbox: {
check: async () => {
const startupApps = await getStartupApps();
return startupApps.includes('tip-of-the-day') || startupApps.includes('tipOfTheDay');
},
toggle: async () => {
const startupApps = await getStartupApps();
const isEnabled = startupApps.includes('tip-of-the-day') || startupApps.includes('tipOfTheDay');
if (isEnabled) {
await removeStartupApp('tip-of-the-day');
await removeStartupApp('tipOfTheDay');
} else {
await addStartupApp('tip-of-the-day');
}
},
},
},
"MENU_DIVIDER",
{
label: "A&gent",
Expand Down Expand Up @@ -144,11 +189,12 @@ export function showClippyContextMenu(event, app) {
new window.ContextMenu(menuItems, event);
}

export function launchClippyApp(app, agentName = currentAgentName) {
export function launchClippyApp(app, agentName = currentAgentName, options = {}) {
if (app) {
window.clippyAppInstance = app;
}
const appInstance = app || window.clippyAppInstance;
const showTip = options.showTip || false;

if (window.clippyAgent) {
// Gracefully hide and remove the current agent before loading a new one
Expand Down Expand Up @@ -218,11 +264,15 @@ export function launchClippyApp(app, agentName = currentAgentName) {
return originalSpeakAndAnimate.call(this, text, animation, newOptions);
};

agent.speakAndAnimate(
"Hey, there. Want quick answers to your questions? Just click me.",
"Explain",
{ useTTS: ttsEnabled },
);
if (showTip) {
showRandomTip(agent);
} else {
agent.speakAndAnimate(
"Hey, there. Want quick answers to your questions? Just click me.",
"Explain",
{ useTTS: ttsEnabled },
);
}

let startX, startY;
agent._el.on("mousedown", (e) => {
Expand Down
82 changes: 6 additions & 76 deletions src/apps/tip-of-the-day/tip-of-the-day-app.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { Application } from '../../system/application.js';
import { tipOfTheDayContent } from './tip-of-the-day.js';
import { apps } from '../../config/apps.js';
import { launchApp } from '../../system/app-manager.js';
import { getStartupApps, addStartupApp, removeStartupApp } from '../../system/startup-manager.js';
import { ICONS } from '../../config/icons.js';

export class TipOfTheDayApp extends Application {
Expand All @@ -28,81 +24,15 @@ export class TipOfTheDayApp extends Application {
}

_createWindow() {
const win = new $Window({
id: this.id,
title: this.title,
outerWidth: this.width,
outerHeight: this.height,
resizable: this.resizable,
minimizeButton: this.minimizeButton,
maximizeButton: this.maximizeButton,
icons: this.icon,
});

win.$content.html(tipOfTheDayContent);
return win;
return null;
}

async _onLaunch() {
const tips = apps.reduce((acc, app) => {
if (app.tips) {
return acc.concat(app.tips);
}
return acc;
}, []);

const contentElement = this.win.$content[0];
let currentTipIndex = Math.floor(Math.random() * tips.length);

const tipTextElement = contentElement.querySelector('#tip-text');
const nextTipButton = contentElement.querySelector('#next-tip');
const closeButton = contentElement.querySelector('.button-group button:last-child');

if (nextTipButton) {
nextTipButton.innerHTML = '';
nextTipButton.appendChild(window.AccessKeys.toFragment('&Next Tip'));
}
if (closeButton) {
closeButton.innerHTML = '';
closeButton.appendChild(window.AccessKeys.toFragment('&Close'));
closeButton.addEventListener('click', () => this.win.close());
}

const displayTip = (tipIndex) => {
if (tipTextElement) {
tipTextElement.innerHTML = tips[tipIndex];
const links = tipTextElement.querySelectorAll('.tip-link');
links.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const appId = link.getAttribute('data-app');
launchApp(appId);
});
});
}
};

displayTip(currentTipIndex);

if (nextTipButton && tipTextElement) {
nextTipButton.addEventListener('click', () => {
currentTipIndex = (currentTipIndex + 1) % tips.length;
displayTip(currentTipIndex);
});
}

const showTipsCheckbox = contentElement.querySelector('#show-tips');
if (showTipsCheckbox) {
const startupApps = await getStartupApps();
showTipsCheckbox.checked = startupApps.includes('tipOfTheDay');
const { launchApp, appManager } = await import('../../system/app-manager.js');
// Delegate to the Assistant (Clippy) to show a tip
await launchApp("clippy", { showTip: true });

showTipsCheckbox.addEventListener('change', async () => {
if (showTipsCheckbox.checked) {
await addStartupApp('tipOfTheDay');
} else {
await removeStartupApp('tipOfTheDay');
}
});
}
// Close this proxy app
appManager.closeApp(this.instanceKey);
}
}