Walk a newcomer through the AI workbench once, and say a line under what shows up later
Step 5 of the lower-barrier plan: notes, never paragraphs on the page. The first time the workbench shows a task, once its window has settled, the spotlight tour walks through New task, Connect an AI, the gates, the list, a task's menu, the timeline and the message box (ui_ai_tour.js); the home section's "How it works" runs it again. What only shows up after the AI has done something gets one line with Got it the first time it appears: under a run's verdict, beside the undo of the AI's canvas changes, under a private task's message box with Hide. Seen ones are remembered in this browser only. The tour engine now skips a target folded to nothing (a tab not shown) and lights only the visible part of a tall one (the timeline), cut to its scrolling box and the window. The task menu button has a class of its own to be found by. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
22cc06495f
commit
47ca8a764a
+5
-1
@@ -606,7 +606,11 @@ covered by the `styles.css` manifest.
|
||||
(the app that came last, a warning when its list is a short one, six starter cards); `ui_ai_starter_sheet.js`
|
||||
holds the cards and the sheet each opens (fields, what happens, what stays, what the AI does not get), which starts a
|
||||
prepared task with its switches set (a new tab; privacy for the private card) through `ui_ai_handoff.js` startTask;
|
||||
a card's tags name only what Anomalous makes sure of. `ui_ai_ready.js` is that
|
||||
a card's tags name only what Anomalous makes sure of. `ui_ai_tour.js` holds a newcomer's notes: the workbench's
|
||||
spotlight tour (new task, connecting, gates, the list, a task's ⋯, the timeline, the message box), by itself the
|
||||
first time a task shows and from the home section's "How it works", and `firstHint`, one line with Got it under a
|
||||
control that only shows up later (a run's verdict, the undo of the AI's canvas changes, a private task's Hide).
|
||||
`ui_ai_ready.js` is that
|
||||
waiting task's one page: the sentence to send the AI, and what the user sent so far; what to say
|
||||
first and pictures go in the message box. The pictures (`web/assets/ai_*.webp`)
|
||||
were made with ChatGPT. `ai_task_view.js` holds
|
||||
|
||||
@@ -86,6 +86,8 @@
|
||||
- On a graphics card of 8 GB or less, Anomalous's upscale-and-redraw ways (the light upscale, the Tile upscale, enlarging only the character) decode the last step with ComfyUI's own VAE Decode (Tiled). A 2x upscale on a 6 GB laptop spent 20 minutes decoding alone and froze the desktop; the tiled decode needs no extra pack, and bigger cards stay as they were.
|
||||
- 主页多了「让 AI 帮你做」:顶上一行写最近连接的 AI 软件(连的是精简工具清单时会提醒它接不了 AMB 里的任务),下面六张卡:先试一次、跑我存的预设、照我说的出图、改一张图、隐私模式出图、看懂我的画布。点开先看说明页:点「开始」会发生什么、什么不会动、AI 拿不到什么、要注意什么;再点「开始」,AMB 把任务建好、开关设好(在新标签页里做,隐私卡直接是隐私任务),给你一句复制给 AI 的话。卡上的小标签只写 AMB 自己能保证的事。
|
||||
- The home page has "Let the AI do it": a line on the AI app that came last (with a warning when it holds a short tool list that cannot take a task handed over here), and six cards: Try it once, Run my preset, Draw what I describe, Change a picture, Draw in privacy mode, Explain my canvas. A card opens a sheet first: what happens on Start, what stays as it is, what the AI does not get, what to mind; Start makes the task with its switches set (a new tab; the privacy card makes a private task) and gives the sentence to copy to the AI. A card's tags name only what Anomalous itself makes sure of.
|
||||
- AI 工作台有了新手引导:第一次打开、里面有任务时,自动带你看一遍新任务、连接 AI、关卡、任务列表、任务的 ⋯、时间线和留言框;以后在主页「让 AI 帮你做」点「怎么用」可以再看。要等 AI 做过事才出现的东西(跑完图的「不太行」、AI 改画布的「撤销这一串」、隐私任务里的「藏」),第一次出现时下面有一行说明,点「知道了」就不再出现。引导遇到看不见的按钮会跳过,很高的一块只框住看得见的部分。
|
||||
- The AI workbench has a newcomer's tour: the first time it shows a task it walks you through New task, Connect an AI, the gates, the list, a task's ⋯, the timeline and the message box; the home page's How it works (under Let the AI do it) runs it again. What shows up only after the AI has done something (Not quite under a run, the AI's canvas undo, Hide in a private task) gets one line the first time, gone after Got it. A tour skips a control that cannot be seen and lights only the visible part of a tall one.
|
||||
|
||||
## v1.63.0 (AI Ways and Skills, Saves, Official Example Workflows, Privacy Mode) — 2026-11-10
|
||||
|
||||
|
||||
@@ -166,10 +166,24 @@ AI 想用几块现成做法一步步画一张图时,先在展台上放一张
|
||||
| 「隐私模式出图」 | 写「画面大意(会发给 AI)」,建一个隐私任务 | 不发图和原文、新标签页 |
|
||||
| 「看懂我的画布」 | 就是画布右键的「交给 AI…」小窗 | 每步可撤销 |
|
||||
|
||||
顶上一行在有 AI 来过以后多一个「怎么用」:转到 AI 工作台,等它打开后走一遍工作台的引导。
|
||||
|
||||
说明页都分几行:点「开始」后、不会动(或 AI 拿不到、管不到)、要注意;必填的没填,按「开始」时下面红字说缺什么。「开始」以后和别处「交给 AI」一样,弹出要发给 AI 的那句话。后端:`GET /anomalous/ai_starters`(只读),建任务还是 `POST /anomalous/ai_tasks`,再按卡片设好新标签页、隐私。
|
||||
|
||||
为什么:新手不敢上手,怕弄坏东西。卡片把常用的几件事准备成任务,说明页先讲清楚会发生什么;标签只写 AMB 自己做得到的保证,不写要靠 AI 自觉的事。
|
||||
|
||||
## AI 工作台的新手引导(`ui_ai_tour.js`)
|
||||
|
||||
第一次打开 AI 工作台、里面有任务时,等窗口打开稳了,自动走一遍聚光灯引导(和主页的界面导览同一种),七步:「新任务」、「连接 AI」、「关卡」、任务列表的筛选、任务标题栏右边的 ⋯、时间线、留言框。看不见的那一步(比如窄窗口里时间线收在别的标签里)跳过。走过一次就不再自己出现(记在这个浏览器里),主页「让 AI 帮你做」的「怎么用」可以再看。
|
||||
|
||||
有些东西要等 AI 做过事才出现,引导走的时候还看不到,所以它们第一次出现时,下面多一行小字,旁边「知道了」,点了就不再出现:
|
||||
|
||||
- 跑完的图下面「效果好」「不太行」:觉得不行就点「不太行」,再点哪里不行,AI 就知道该改哪儿。
|
||||
- AI 改画布的一串步骤旁有「撤销这一串」时:点它就能退回去。
|
||||
- 隐私任务的留言框里有「藏」时:选中几个字再点「藏」,这几个字 AI 只拿到代号。
|
||||
|
||||
为什么:不在页面上写大段说明。界面一直在的东西用引导讲一次;后来才出现的东西在它旁边说一句。
|
||||
|
||||
## 任务单(`ui_job_sheet.js`)
|
||||
|
||||
右键节点或画布空白处「任务单(批量换提示词)」打开,在画布右下角,ComfyUI 的画布按钮上面。用同一张画布连着跑很多次:观众要的“一次贴几十条提示词,每条跑几次”和“运行间隔”都是它。
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""A newcomer's notes on the AI workbench (web/modules/ui_ai_tour.js): every tour step and every first-time
|
||||
line has its words in both languages, and each first-time line is used where its control is drawn."""
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
PLUGIN = Path(__file__).resolve().parents[1]
|
||||
MODULES = PLUGIN / "web" / "modules"
|
||||
|
||||
|
||||
class TourWordsTests(unittest.TestCase):
|
||||
def test_every_step_and_line_has_both_languages(self):
|
||||
tour = (MODULES / "ui_ai_tour.js").read_text(encoding="utf-8")
|
||||
words = (MODULES / "locales.js").read_text(encoding="utf-8")
|
||||
steps = re.findall(r"step\('(\w+)'", tour)
|
||||
self.assertEqual(steps, ["new", "connect", "gates", "list", "menu", "timeline", "message"])
|
||||
keys = [f"aiTour_{name}_{part}" for name in steps for part in ("title", "body")] + ["aiFirstHintDone"]
|
||||
for key in keys:
|
||||
self.assertEqual(words.count(f" {key}: "), 2, key)
|
||||
|
||||
def test_each_first_time_line_sits_under_its_control(self):
|
||||
uses = {"verdict": "ui_ai_verdict.js", "undo": "ui_ai_chain.js", "hide": "ui_ai_message.js"}
|
||||
words = (MODULES / "locales.js").read_text(encoding="utf-8")
|
||||
for hint, module in uses.items():
|
||||
code = (MODULES / module).read_text(encoding="utf-8")
|
||||
key = re.search(rf"firstHint\('{hint}', t\('(\w+)'\)\)", code)
|
||||
self.assertIsNotNone(key, module)
|
||||
self.assertEqual(words.count(f" {key.group(1)}: "), 2, key.group(1))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1849,6 +1849,25 @@ export const i18n = {
|
||||
aiConnectCopyRules: "复制这段规则",
|
||||
aiConnectRules: "在 Anomalous Model Browser(AMB)的任务里:\n1. 新开对话或拿不准现在到哪一步时,先调用 get_situation:任务、画布还缺哪几根线、下一步做什么,一次就知道。\n2. 先用 AMB 的 MCP 工具:模型、图片、节点、画布、出图和进度都有对应的工具(search_models、get_image_info、zoom_image、find_nodes、run_workflow、run_status 等)。不要自己去读 ComfyUI 的文件夹、源码或接口。日常出图用 run_workflow,带上 workflow(存好的工作流名)和要改的提示词、种子或尺寸,一步完成。\n3. 只通过运行 ComfyUI 工作流来改图片;绝不用自己的代码去改、裁、涂或覆盖图片文件。\n4. 要问我的事用 ask_user,说明和结果用 report,都在 AMB 里。\n5. 如果还是用了别的工具(命令、脚本、读写文件),紧接着调用 log_outside,写明做了什么、为什么、动了哪些文件。",
|
||||
aiConnectLocal: "只有运行 ComfyUI 的这台电脑能连接。加好以后,在 AI 软件里直接说要做什么就行;连过一次以后不用再加。",
|
||||
aiFirstHintDone: "知道了",
|
||||
aiFirstVerdict: "觉得不行就点「不太行」,再点哪里不行,AI 就知道该改哪儿。",
|
||||
aiFirstUndo: "AI 改画布的这几步,点「撤销这一串」就能退回去。",
|
||||
aiFirstHide: "选中几个字再点「藏」,这几个字 AI 只拿到代号。",
|
||||
aiStartHowItWorks: "怎么用",
|
||||
aiTour_new_title: "新任务",
|
||||
aiTour_new_body: "要交给 AI 一件事,先点这里开一个任务,再把那句话发给你的 AI 软件,它就会接着这个任务做。首页的「让 AI 帮你做」也能直接开。",
|
||||
aiTour_connect_title: "连接 AI",
|
||||
aiTour_connect_body: "AI 软件第一次用 AMB 时,从这里拿地址和要发给它的那句话。连过一次就不用再连。",
|
||||
aiTour_gates_title: "关卡",
|
||||
aiTour_gates_body: "AI 做哪些事要先问你:改画布、用显卡、下载模型、挪文件……刚开始用,可以都设成先问。",
|
||||
aiTour_list_title: "任务列表",
|
||||
aiTour_list_body: "每件交给 AI 的事是一条,最新的在上面,可以按进行中、等你回答、出错筛一下。不要的任务在它右边的 ⋯ 里移除,只删记录。",
|
||||
aiTour_menu_title: "这个任务的 ⋯",
|
||||
aiTour_menu_body: "改名、隐私模式(AI 看不到这个任务的图和提示词原文)、发给 AI 了什么,都在这里。",
|
||||
aiTour_timeline_title: "时间线",
|
||||
aiTour_timeline_body: "AI 的每一步按时间排在这里,点开能看细节。它改画布的步骤都能撤销,不怕改坏。",
|
||||
aiTour_message_title: "留言框",
|
||||
aiTour_message_body: "想跟 AI 说话就写在这里,它下一步就会收到;也能附上图库里的图。",
|
||||
aiStartTitle: "让 AI 帮你做",
|
||||
aiStartShortList: "{client} 连的是精简工具清单,接不了 AMB 里的任务:在「连接 AI」里选「全部工具」,再让它重新连接。",
|
||||
aiStartGo: "开始",
|
||||
@@ -5826,6 +5845,25 @@ export const i18n = {
|
||||
aiConnectCopyRules: "Copy the rules",
|
||||
aiConnectRules: "In Anomalous Model Browser (AMB) tasks:\n1. In a new chat, or unsure where things stand, call get_situation first: the task, what the canvas still lacks and the next step, in one answer.\n2. Use AMB's MCP tools first: models, pictures, nodes, the canvas, runs and their progress all have tools (search_models, get_image_info, zoom_image, find_nodes, run_workflow, run_status...). Do not read ComfyUI's folders, source or routes yourself. For a daily run use run_workflow with workflow (a saved one's name) and the prompt, seed or size to change: one call.\n3. Change pictures only by running ComfyUI workflows; never edit, crop, paint over or overwrite picture files with code of your own.\n4. Ask me with ask_user and explain with report, in AMB.\n5. If you still use another tool (a command, a script, files), call log_outside right after: what you ran, why, and which files.",
|
||||
aiConnectLocal: "Only this computer, where ComfyUI runs, can connect. Once added, just tell the AI app what to do; it stays added.",
|
||||
aiFirstHintDone: "Got it",
|
||||
aiFirstVerdict: "Not right? Press Not quite and then what is off: the AI knows what to fix.",
|
||||
aiFirstUndo: "Press the undo beside these steps to take the AI's canvas changes back.",
|
||||
aiFirstHide: "Select a few words and press Hide: the AI gets a token for them.",
|
||||
aiStartHowItWorks: "How it works",
|
||||
aiTour_new_title: "New task",
|
||||
aiTour_new_body: "To hand the AI something, start a task here, then send the sentence to your AI app: it goes on with this task. The home page's Let the AI do it starts one too.",
|
||||
aiTour_connect_title: "Connect an AI",
|
||||
aiTour_connect_body: "The first time an AI app uses Anomalous, take the address and the sentence for it from here. Once is enough.",
|
||||
aiTour_gates_title: "Gates",
|
||||
aiTour_gates_body: "What the AI asks you before doing: changing the canvas, using the graphics card, downloading models, moving files… At first you may set them all to ask.",
|
||||
aiTour_list_title: "The tasks",
|
||||
aiTour_list_body: "Each thing handed to the AI is a row, the newest on top; filter by running, waiting for you or failed. Remove one you do not need from its ⋯: only the record goes.",
|
||||
aiTour_menu_title: "This task's ⋯",
|
||||
aiTour_menu_body: "Rename, privacy mode (the AI sees none of this task's pictures and prompt words), what was sent to the AI: all here.",
|
||||
aiTour_timeline_title: "The timeline",
|
||||
aiTour_timeline_body: "Every step of the AI in order, each opening to its details. Its canvas changes all undo, so nothing is lost.",
|
||||
aiTour_message_title: "The message box",
|
||||
aiTour_message_body: "Write to the AI here: it gets the message with its next step. Pictures from the gallery can go along.",
|
||||
aiStartTitle: "Let the AI do it",
|
||||
aiStartShortList: "{client} holds a short tool list and cannot take a task handed over in Anomalous: pick Every tool in Connect an AI, then reconnect it.",
|
||||
aiStartGo: "Start",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import { app } from '../../../scripts/app.js';
|
||||
import { translate as t } from './locales.js';
|
||||
import { firstHint } from './ui_ai_tour.js';
|
||||
import { timeLabel } from './activity_log.js';
|
||||
import { canUndo, undoEntry } from './canvas_undo.js';
|
||||
import { anomalousConfirm } from './ui_dialog.js';
|
||||
@@ -89,10 +90,12 @@ export function chainRow(steps, number, ctx) {
|
||||
open.append(el('span', 'anomalous-ai-chain-title', t('aiChain', { count: steps.length })));
|
||||
if (ctx.live) open.append(el('span', 'anomalous-ai-chain-last', t('aiChainLast', { what: t(toolKey(steps.at(-1).tool)) })));
|
||||
head.append(open);
|
||||
let undoHint = null;
|
||||
if (changesOf(steps, ctx.entries).some(entry => canUndo(app, entry))) {
|
||||
head.append(iconButton('anomalous-ai-chain-undo', 'undo', t('aiChainUndo'), async () => {
|
||||
if (await undoChanges(steps, ctx.entries, t('aiChainUndo'), 'aiChainUndoConfirm')) ctx.changed();
|
||||
}));
|
||||
undoHint = firstHint('undo', t('aiFirstUndo'));
|
||||
}
|
||||
const chips = el('div', 'anomalous-ai-chain-chips');
|
||||
for (const { tool, kind, count } of chainTools(steps)) {
|
||||
@@ -118,7 +121,7 @@ export function chainRow(steps, number, ctx) {
|
||||
show(true);
|
||||
};
|
||||
show(opened.has(key));
|
||||
body.append(head, chips, inside);
|
||||
body.append(head, ...(undoHint ? [undoHint] : []), chips, inside);
|
||||
const [first, last] = [timeLabel(steps[0].at), timeLabel(steps.at(-1).at)];
|
||||
row.append(body, el('div', 'anomalous-ai-step-when', first === last ? first : `${first}–${last}`));
|
||||
return row;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
import { translate as t } from './locales.js';
|
||||
import { firstHint } from './ui_ai_tour.js';
|
||||
import { mentions } from './ui_ai_mention.js';
|
||||
import { galleryPicker } from './ui_ai_gallery_pick.js';
|
||||
import { takePictures } from './ui_ai_drop.js';
|
||||
@@ -111,6 +112,13 @@ export function messageBox(onSend, source) {
|
||||
field.addEventListener('focus', shown);
|
||||
shown();
|
||||
line.insertBefore(hide, send);
|
||||
const hideHint = firstHint('hide', t('aiFirstHide'));
|
||||
if (hideHint) {
|
||||
box.insertBefore(hideHint, note);
|
||||
const sync = () => { hideHint.hidden = hide.hidden; };
|
||||
field.addEventListener('focus', sync);
|
||||
sync();
|
||||
}
|
||||
takePictures(box, field, { add: item => at.add(item), room, say: (text) => {
|
||||
note.textContent = text;
|
||||
note.classList.add('is-error');
|
||||
|
||||
@@ -12,6 +12,7 @@ import { openConnectGuide } from './ui_ai_connect.js';
|
||||
import { handCanvasToAi } from './ui_ai_canvas_hand.js';
|
||||
import { icon } from './ui_ai_icons.js';
|
||||
import { STARTERS, cardTags, openStarter } from './ui_ai_starter_sheet.js';
|
||||
import { tourWhenShown } from './ui_ai_tour.js';
|
||||
|
||||
function el(tag, className, text) {
|
||||
const node = document.createElement(tag);
|
||||
@@ -28,11 +29,12 @@ function button(className, label, onClick) {
|
||||
}
|
||||
|
||||
/** The line about the AI app: who came last, or none yet; a short tool list said apart. */
|
||||
function appLine(app) {
|
||||
function appLine(owner, app) {
|
||||
const line = el('div', `anomalous-ai-start-app${app ? ' is-known' : ''}`);
|
||||
line.append(el('span', 'anomalous-ai-visit-dot'),
|
||||
el('span', '', app ? t('aiLastVisit', { client: app.client, when: `${dayLabel(app.when)} ${timeLabel(app.when)}` }) : t('aiNoVisit')),
|
||||
button('anomalous-home-link', t('aiConnect'), () => openConnectGuide()));
|
||||
if (app) line.append(button('anomalous-home-link', t('aiStartHowItWorks'), () => { owner.goTo('ai'); setTimeout(tourWhenShown, 700); }));
|
||||
const lines = [line];
|
||||
if (app && (app.tools === 'core' || app.tools === 'mine')) lines.push(el('div', 'anomalous-ai-start-warn', t('aiStartShortList', { client: app.client })));
|
||||
return lines;
|
||||
@@ -61,7 +63,7 @@ export function renderAiStarters(owner, after) {
|
||||
const head = el('div', 'anomalous-ai-start-head');
|
||||
const title = el('span', 'anomalous-ai-start-title');
|
||||
title.append(icon('sparkle', 18), el('span', '', t('aiStartTitle')));
|
||||
head.append(title, ...appLine(data.app));
|
||||
head.append(title, ...appLine(owner, data.app));
|
||||
const grid = el('div', 'anomalous-ai-start-cards');
|
||||
grid.append(...STARTERS.map(starter => card(owner, starter, data)));
|
||||
section.append(head, grid);
|
||||
|
||||
@@ -51,7 +51,7 @@ export function openTaskMenu(anchor, task, { tasks, rename, remove, changed }) {
|
||||
export function taskMenu(task, actions) {
|
||||
const more = document.createElement('button');
|
||||
more.type = 'button';
|
||||
more.className = 'anomalous-ai-tool is-icon';
|
||||
more.className = 'anomalous-ai-tool is-icon anomalous-ai-task-menu';
|
||||
more.textContent = '⋯';
|
||||
more.title = t('notebookMore');
|
||||
more.onclick = () => openTaskMenu(more, task, actions);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Notes for a newcomer to the AI workbench, in two kinds and never as paragraphs on the page:
|
||||
* - the workbench's tour (the spotlight tour of ui_spotlight_tour.js with its own steps): a new task,
|
||||
* connecting, the gates, the list, a task's ⋯, the timeline and the message box. It runs by itself the
|
||||
* first time the workbench shows a task, and again from the home page's "How it works" (startAiTour).
|
||||
* - firstHint(): one line under a control that only shows up later (a run's verdict, undo for the AI's
|
||||
* canvas changes, the Hide button of a private task), shown until the user says Got it.
|
||||
* What was seen is remembered in this browser only; without storage the notes simply come again.
|
||||
*/
|
||||
|
||||
import { translate as t } from './locales.js';
|
||||
import { isSpotlightTourActive, startSpotlightTour } from './ui_spotlight_tour.js';
|
||||
|
||||
const TOUR_KEY = 'anomalous_ai_tour_seen';
|
||||
const HINTS_KEY = 'anomalous_ai_hints_seen';
|
||||
const step = (id, targetSelector, icon, position) => ({ id, targetSelector, icon, position, titleKey: `aiTour_${id}_title`, bodyKey: `aiTour_${id}_body` });
|
||||
export const AI_TOUR = Object.freeze([
|
||||
step('new', '.anomalous-ai-tasks .anomalous-ai-new-task', '➕', 'right'),
|
||||
step('connect', '.anomalous-ai-tasks-ways .anomalous-ai-way:nth-child(1)', '🔌', 'right'),
|
||||
step('gates', '.anomalous-ai-tasks-ways .anomalous-ai-way:nth-child(2)', '🛡️', 'right'),
|
||||
step('list', '.anomalous-ai-tasks .anomalous-ai-task-filters', '📋', 'right'),
|
||||
step('menu', '.anomalous-ai-task-head .anomalous-ai-task-menu', '⋯', 'bottom'),
|
||||
step('timeline', '.anomalous-ai-timeline', '🕘', 'left'),
|
||||
step('message', '.anomalous-ai-task .anomalous-ai-message', '💬', 'top'),
|
||||
]);
|
||||
|
||||
function seen(key) {
|
||||
try {
|
||||
const ids = JSON.parse(localStorage.getItem(key) || '[]');
|
||||
return new Set(Array.isArray(ids) ? ids : []);
|
||||
} catch (_) {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function remember(key, id) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify([...seen(key).add(id)]));
|
||||
} catch (_) {
|
||||
// a convenience only: the note comes again next time
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the workbench shows a task at its full size (not mid-animation, not folded into tabs). */
|
||||
const settled = page => (page.querySelector('.anomalous-ai-timeline')?.getBoundingClientRect().width || 0) > 200;
|
||||
|
||||
export function startAiTour() {
|
||||
return startSpotlightTour(null, { steps: AI_TOUR });
|
||||
}
|
||||
|
||||
/** The tour as soon as the workbench shows a task (after going there from the home page); gives up after a few seconds. */
|
||||
export function tourWhenShown(tries = 20) {
|
||||
const page = document.querySelector('.anomalous-ai');
|
||||
if (page && settled(page)) {
|
||||
remember(TOUR_KEY, 'workbench');
|
||||
startAiTour();
|
||||
} else if (tries > 0) {
|
||||
setTimeout(() => tourWhenShown(tries - 1), 200);
|
||||
}
|
||||
}
|
||||
|
||||
/** The tour by itself, once, when `page` (the workbench) shows a task and nothing else is in front; after the
|
||||
* window has finished opening, since a tour started in its animation points at a squeezed layout. */
|
||||
export function aiTourOnce(page) {
|
||||
if (seen(TOUR_KEY).has('workbench') || isSpotlightTourActive() || !page?.isConnected) return;
|
||||
setTimeout(() => {
|
||||
if (!page.isConnected || isSpotlightTourActive() || !settled(page)) return;
|
||||
if (startAiTour()) remember(TOUR_KEY, 'workbench');
|
||||
}, 900);
|
||||
}
|
||||
|
||||
/** The line for `id` (words `text`) until the user said Got it, or null; one of each on the page at a time. */
|
||||
export function firstHint(id, text) {
|
||||
if (seen(HINTS_KEY).has(id) || document.querySelector(`.anomalous-ai-first-hint[data-hint="${id}"]`)) return null;
|
||||
const line = document.createElement('div');
|
||||
line.className = 'anomalous-ai-first-hint';
|
||||
line.dataset.hint = id;
|
||||
const words = document.createElement('span');
|
||||
words.textContent = text;
|
||||
const done = document.createElement('button');
|
||||
done.type = 'button';
|
||||
done.className = 'anomalous-ai-first-hint-done';
|
||||
done.textContent = t('aiFirstHintDone');
|
||||
done.onclick = () => {
|
||||
remember(HINTS_KEY, id);
|
||||
line.remove();
|
||||
};
|
||||
line.append(words, done);
|
||||
return line;
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { translate as t } from './locales.js';
|
||||
import { firstHint } from './ui_ai_tour.js';
|
||||
import { anomalousAlert } from './ui_dialog.js';
|
||||
import { icon } from './ui_ai_icons.js';
|
||||
|
||||
@@ -64,5 +65,7 @@ export function verdictRow(step, taskId) {
|
||||
}
|
||||
row.append(issues);
|
||||
}
|
||||
const hint = !step.verdict && firstHint('verdict', t('aiFirstVerdict'));
|
||||
if (hint) row.append(hint);
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
activityIds, duration, groupSteps, stepDetail, taskCounts, taskResults, taskState, toolKey,
|
||||
} from './ai_task_view.js';
|
||||
import { taskTitle } from './ai_task_title.js';
|
||||
import { aiTourOnce } from './ui_ai_tour.js';
|
||||
|
||||
const NARROW_PX = 720;
|
||||
const SHORT_PX = 880; // a window this low (not maximised) keeps less room around the task's head
|
||||
@@ -543,7 +544,7 @@ export function renderAiWorkbench(owner, panel) {
|
||||
owner.headerLeadTools?.replaceChildren(listToggle(state.page, toggleTasks));
|
||||
owner.railNews?.('ai', false);
|
||||
hidePanelToast();
|
||||
void refresh();
|
||||
void refresh().then(() => aiTourOnce(state.page)); // a newcomer's tour, once (ui_ai_tour.js)
|
||||
pollVisible();
|
||||
}
|
||||
|
||||
|
||||
@@ -297,7 +297,10 @@ export function ensureTourStyles() {
|
||||
|
||||
function resolveStepTarget(step) {
|
||||
if (typeof document === 'undefined') return null;
|
||||
const shown = el => Boolean(el?.isConnected && el.getClientRects().length); // hidden targets are skipped
|
||||
const shown = (el) => { // hidden targets are skipped, and so is one folded to nothing (a tab not shown)
|
||||
const box = el?.isConnected && el.getClientRects().length ? el.getBoundingClientRect() : null;
|
||||
return Boolean(box && box.width > 0 && box.height > 0);
|
||||
};
|
||||
let el = document.querySelector(step.targetSelector);
|
||||
if (!shown(el) && step.fallbackSelector) {
|
||||
el = document.querySelector(step.fallbackSelector);
|
||||
@@ -305,6 +308,19 @@ function resolveStepTarget(step) {
|
||||
return shown(el) ? el : null;
|
||||
}
|
||||
|
||||
/** The part of `rect` (target's) that can be seen: a tall target (a timeline) is cut to its scrolling box and the window. */
|
||||
function visiblePart(target, rect) {
|
||||
let top = Math.max(rect.top, 0);
|
||||
let bottom = Math.min(rect.bottom, window.innerHeight);
|
||||
for (let node = target.parentElement; node && node !== document.body; node = node.parentElement) {
|
||||
if (!/(auto|scroll)/.test(getComputedStyle(node).overflowY)) continue;
|
||||
const box = node.getBoundingClientRect();
|
||||
[top, bottom] = [Math.max(top, box.top), Math.min(bottom, box.bottom)];
|
||||
break;
|
||||
}
|
||||
return bottom > top ? { left: rect.left, right: rect.right, width: rect.width, top, bottom, height: bottom - top } : rect;
|
||||
}
|
||||
|
||||
function computeCardPosition(rect, position, cardWidth = 330, cardHeight = 220) {
|
||||
const margin = 12;
|
||||
const padding = 16;
|
||||
@@ -399,7 +415,7 @@ export function startSpotlightTour(owner, { steps = TOUR_STEPS, onClose = null }
|
||||
// Scroll target into view if out of sight
|
||||
target.scrollIntoView?.({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
||||
|
||||
const rect = target.getBoundingClientRect?.() || { left: 0, top: 0, width: 100, height: 40, right: 100, bottom: 40 };
|
||||
const rect = target.getBoundingClientRect ? visiblePart(target, target.getBoundingClientRect()) : { left: 0, top: 0, width: 100, height: 40, right: 100, bottom: 40 };
|
||||
const buffer = 5;
|
||||
|
||||
// Position spotlight box smoothly
|
||||
|
||||
+1
-1
@@ -53,4 +53,4 @@
|
||||
@import url("./styles/52-model-guide.css?v=20261108-guide");
|
||||
@import url("./styles/53-ai-plan.css?v=20261109-plan");
|
||||
@import url("./styles/54-job-sheet.css?v=20261109-sheet");
|
||||
@import url("./styles/55-ai-starters.css?v=20261113-start");
|
||||
@import url("./styles/55-ai-starters.css?v=20261113-tour");
|
||||
|
||||
@@ -79,3 +79,28 @@
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* A newcomer's line under a control that shows up later (ui_ai_tour.js firstHint) */
|
||||
.anomalous-ai-first-hint {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px 10px;
|
||||
margin-top: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--amb-link) 10%, transparent);
|
||||
color: var(--amb-text-soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.anomalous-ai-first-hint[hidden] { display: none; }
|
||||
.anomalous-ai-first-hint-done {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--amb-link);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.anomalous-ai-first-hint-done:hover { text-decoration: underline; }
|
||||
|
||||
Reference in New Issue
Block a user