Show whether the AI has each answer, and let a question be skipped
Every answer, message, set of marks and handed picture shows a pill: sent and waiting, then the AI has it with the time it did (delivered_at). Panel questions can be skipped; the AI reads skipped: true. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9abd596186
commit
4dcda6edf8
+4
-1
@@ -307,7 +307,10 @@ covered by the `styles.css` manifest.
|
||||
can also write to the AI on a task (`POST /anomalous/ai_tasks/{id}/message`, a `user` step);
|
||||
`get_answers` brings those messages too, and with no panel named waits for one; unread answers and user messages are also piggybacked once onto the next successful MCP tool response (`ai_panels.attach_pending`) so AI apps that do not poll `get_answers` still receive them immediately.
|
||||
`web/modules/ui_ai_panel.js` draws a panel in the timeline (textContent only) and the toast
|
||||
when the AI shows one while the user is elsewhere. The stage blocks are data the page lays
|
||||
when the AI shows one while the user is elsewhere; a question can be skipped (`{skip: true}`,
|
||||
the AI reads `skipped`). `web/modules/ui_ai_delivery.js` is the pill under each answer and
|
||||
each thing the user gave the AI: sent and waiting, or the AI has it at `delivered_at` (set
|
||||
when `ai_panels._collect` hands it over). The stage blocks are data the page lays
|
||||
out, never drawings of the AI's own (no SVG is taken): marks on pictures (circles or boxes at
|
||||
fractions of the picture, a label that zooms there), the slider (before and after over each
|
||||
other), picks (pictures with a reason, "Take this one", "Put on the canvas" through ComfyUI's
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
- AI 工作台任务工具栏新增小巧的“运行状态”按钮与浮窗(移除遮挡时间线的常驻大横条):点开即可查看真实运行状态、排队、节点进度、用时和结果,运行中直接在按钮上显示进度百分比;没有收到运行信息时明确提示,不再空转显示“AI 正在工作”或把沉默当成完成。页面可见时自动补查状态。运行工具默认等待 15 秒并返回实际结果;外部提交可用 `track_run` 接入准确的运行 ID,不会重新出图。
|
||||
- The AI workbench now places confirmed execution state, queue/node progress, elapsed time and results inside a compact toolbar button and popover (instead of a tall sticky banner), with visible-page refresh. Untracked work and silence are explicit. Run calls wait briefly for real results; `track_run` attaches external submission IDs without resubmitting.
|
||||
|
||||
### 回答发出去了没有,一眼看清 / See at a glance whether the AI has your answer
|
||||
- 面板上每个回答、你的留言、批注、发给 AI 的图片下面都有醒目的状态标签:琥珀色 **已发送 · 等 AI 来取**,AI 拿到后变成绿色 **AI 已收到 · 时间**(取代原来那行灰色小字)。鼠标停在“已发送”上会提示 AI 停下时该怎么办。
|
||||
- 面板上的问题可以点 **这题跳过**:可选的问题不再一直显示“等你回答”,AI 会收到“你跳过了”,自己拿主意。
|
||||
- Each answer, message, set of marks and picture handed to the AI shows a pill: "Sent · waiting for the AI", then "The AI has it" with the time. Panel questions can be skipped; the AI reads `skipped: true`.
|
||||
|
||||
### 看得见 AI 跑了什么、专用标签页、图片直接给 AI / What the AI ran, its own tab, pictures straight to the AI
|
||||
- 每次运行下面都有 **节点结构**:一行写明底模、几个 LoRA、尺寸、步数,点开是方块图(LoRA 带权重、正负提示词、采样器设置……,点方块定位到画布上的节点),还用黄色小块标出 **比上次改了什么**,比如“步数 20 → 30”“LoRA 权重 0.65 → 0.8”“新增「加载LoRA」”。图是 AMB 按 ComfyUI 实际收到的工作流画的,不靠 AI 描述;AI 讲解时用新工具 `show_run_map` 一次把它放上展台。
|
||||
- **AI 的专用标签页**(默认开):AI 第一次改画布前,AMB 把你正开着的工作流复制到以任务命名的新标签页,之后它只在那一页改和运行;你切去别处,它下次动手前会切回来;你原来的工作流一笔不动。任务标题栏能切换“专用标签页 / 当前画布”、点标签页名字跳过去;关卡窗口里设定新任务的默认做法。
|
||||
|
||||
+8
-6
@@ -368,8 +368,8 @@ def update(panel_id, title=None, blocks=None, closed=None):
|
||||
return panel
|
||||
|
||||
|
||||
def answer(task_id, step_id, block_id, choice=None, text=None, now=None):
|
||||
"""The user's answer to one block of a panel (from the page). Returns the step's answers."""
|
||||
def answer(task_id, step_id, block_id, choice=None, text=None, now=None, skip=False):
|
||||
"""The user's answer to one block of a panel (from the page), or `skip` it. Returns the step's answers."""
|
||||
with _answered:
|
||||
task = ai_tasks._load().get(task_id)
|
||||
step = next((item for item in (task or {}).get("steps") or [] if item.get("id") == step_id), None)
|
||||
@@ -383,7 +383,9 @@ def answer(task_id, step_id, block_id, choice=None, text=None, now=None):
|
||||
if block_id in step.setdefault("answers", {}):
|
||||
raise PanelError("Already answered")
|
||||
entry = {"at": time.time() if now is None else now, "delivered": False}
|
||||
if block["type"] == "choices" and block.get("other") and not choice and isinstance(text, str) and text.strip():
|
||||
if skip:
|
||||
entry["skipped"] = True
|
||||
elif block["type"] == "choices" and block.get("other") and not choice and isinstance(text, str) and text.strip():
|
||||
entry.update(choice=["other"], labels=["other"], text=_text(text, 2000, "text", True))
|
||||
elif block["type"] in ("choices", "picks"):
|
||||
picked = choice if isinstance(choice, list) else [choice]
|
||||
@@ -445,7 +447,7 @@ def _collect(client, panel_id, task_id=None):
|
||||
continue
|
||||
for step in task.get("steps") or []:
|
||||
if step.get("kind") == "user" and not step.get("delivered") and (panel_id is None or task is panel_task):
|
||||
step["delivered"] = True
|
||||
step.update(delivered=True, delivered_at=time.time())
|
||||
changed.add(task["id"])
|
||||
message = {"id": step["id"], "task": task["id"], "text": step["text"], "at": step["at"]}
|
||||
if step.get("tool") == "user_activity":
|
||||
@@ -487,10 +489,10 @@ def _collect(client, panel_id, task_id=None):
|
||||
continue
|
||||
if given.get("delivered"):
|
||||
continue
|
||||
given["delivered"] = True
|
||||
given.update(delivered=True, delivered_at=time.time())
|
||||
changed.add(task["id"])
|
||||
item = {"panel": panel["id"], "question": block["id"], "asked": block.get("question", "")}
|
||||
item.update({key: given[key] for key in ("choice", "labels", "text") if key in given})
|
||||
item.update({key: given[key] for key in ("choice", "labels", "text", "skipped") if key in given})
|
||||
out.append(item)
|
||||
for tid in changed:
|
||||
ai_tasks._save(tasks[tid])
|
||||
|
||||
@@ -62,7 +62,7 @@ async def api_change(request):
|
||||
|
||||
|
||||
async def api_answer(request):
|
||||
"""POST /anomalous/ai_tasks/{id}/answer: {step, block, choice: [ids] | text} - the user's answer on a panel."""
|
||||
"""POST /anomalous/ai_tasks/{id}/answer: {step, block, choice: [ids] | text | skip: true} - the user's answer on a panel."""
|
||||
from . import ai_panels
|
||||
|
||||
try:
|
||||
@@ -73,7 +73,7 @@ async def api_answer(request):
|
||||
return web.json_response({"error": "Expected {step, block, choice | text}"}, status=400)
|
||||
try:
|
||||
answers = ai_panels.answer(request.match_info["task_id"], str(body.get("step")), str(body.get("block")),
|
||||
body.get("choice"), body.get("text"))
|
||||
body.get("choice"), body.get("text"), skip=body.get("skip") is True)
|
||||
except ai_panels.PanelError as error:
|
||||
return web.json_response({"error": str(error)}, status=400)
|
||||
return web.json_response({"answers": answers})
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ def _words(step):
|
||||
def _answered(step):
|
||||
"""What the user already answered on a panel step: [{question, answer}]."""
|
||||
blocks = {block["id"]: block for block in step["panel"]["blocks"]}
|
||||
return [{"question": blocks.get(block, {}).get("question", ""), "answer": "、".join(given.get("labels") or [given.get("text", "")])}
|
||||
return [{"question": blocks.get(block, {}).get("question", ""), "answer": "(skipped)" if given.get("skipped") else "、".join(given.get("labels") or [given.get("text", "")])}
|
||||
for block, given in step.get("answers", {}).items()]
|
||||
|
||||
|
||||
|
||||
@@ -87,7 +87,9 @@ def ask_user(ctx, question, options=None, multiple=False, other=True, wait_secon
|
||||
"note": "The user has not answered yet. Keep waiting with get_answers(panel, wait_seconds=55); "
|
||||
"do not ask the same in your own chat, they answer in Anomalous."}
|
||||
out = {"answered": True, "panel": panel["id"]}
|
||||
if given.get("text"):
|
||||
if given.get("skipped"):
|
||||
out.update(skipped=True, note="The user skipped this question: go on with your own judgement.")
|
||||
elif given.get("text"):
|
||||
out["text"] = given["text"]
|
||||
if given.get("choice") == ["other"]:
|
||||
out["note"] = "None of your options: the user wrote their own answer."
|
||||
@@ -313,7 +315,8 @@ TASK_TOOLS = dict([
|
||||
"themselves in Anomalous while you worked, keep it), since you last asked; each once. For one "
|
||||
"panel (and its task's messages), or all of yours. wait_seconds (up to 55) waits when nothing is there "
|
||||
"yet: for that panel's open questions, or with no panel for anything the user sends. still_waiting "
|
||||
"lists the questions not answered. Check it between steps of longer work.",
|
||||
"lists the questions not answered; skipped=true is a question the user chose not to answer (go on with your "
|
||||
"own judgement). Check it between steps of longer work.",
|
||||
get_answers, {"panel": {"type": "string"},
|
||||
"wait_seconds": {"type": "number", "minimum": 0, "maximum": ai_panels.MAX_WAIT, "default": 0},
|
||||
"task": _TASK_PROP}),
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ claude mcp add --transport http anomalous http://127.0.0.1:8188/anomalous/mcp
|
||||
- 任务可以改名、从列表移除(只删记录,图和画布不受影响)。记录存在 ComfyUI 的 user 文件夹里,保留最近 300 个。
|
||||
- 只看得到经过这个接口的事。AI 绕开它做的,可以让它用 `report` 报告。
|
||||
|
||||
**AI 画的面板**(`show_panel`)显示在工作台右边的**展台**上,后面再多步骤也不会被顶走;时间线上只留一行“在展台打开 ▸”。展台默认跟着最新的面板,你翻回旧面板就停在你看的那张;窗口窄的时候,时间线和展台变成两个标签页切换。左边的任务列表可以点 ‹ 收起,腾出地方。AI 可以用插件提供的“积木”拼面板:标题和文字(也可以是提示、警告)、图片(一排、网格,或者两张并排对比)、你的模型(带封面的卡片,点了打开)、表格、列表、参数、进度条。它还能**问你**:几个按钮(单选或多选)、一行输入框。你的回答会交出去,等 AI 用 `get_answers` 来取(它可以等你最多一分钟);每个问题下面都写着 AI 有没有看到。AMB 没法主动叫醒 AI,所以回答之后要等它下一次来问。
|
||||
**AI 画的面板**(`show_panel`)显示在工作台右边的**展台**上,后面再多步骤也不会被顶走;时间线上只留一行“在展台打开 ▸”。展台默认跟着最新的面板,你翻回旧面板就停在你看的那张;窗口窄的时候,时间线和展台变成两个标签页切换。左边的任务列表可以点 ‹ 收起,腾出地方。AI 可以用插件提供的“积木”拼面板:标题和文字(也可以是提示、警告)、图片(一排、网格,或者两张并排对比)、你的模型(带封面的卡片,点了打开)、表格、列表、参数、进度条。它还能**问你**:几个按钮(单选或多选)、一行输入框。你的回答会交出去,等 AI 用 `get_answers` 来取(它可以等你最多一分钟);每个问题下面有一个状态标签:琥珀色的 **已发送 · 等 AI 来取**,AI 拿到后变成绿色的 **AI 已收到 · 时间**。你的留言、批注、发给它的图片也是一样。不想答的题点 **这题跳过**,AI 会知道你跳过了,自己拿主意。AMB 没法主动叫醒 AI,所以回答之后要等它下一次来问。
|
||||
|
||||
**展台积木**(1.62):AI 还可以在图上**画圈标注**(圈上是编号,标签列在图下面,点标签放大那一块)、摆**拉帘对比**(两张图叠在一起,拖中间的线)、摆**选片盘**(每张图带 AI 的理由和“就选这张”;“放回画布”会在新标签页里打开这张图里存的工作流,也可以直接拖到画布上)、画**示意图**(框和箭头由插件排版;带 ◎ 的框点一下定位到画布上的节点)。AI 只给位置和文字,圈、线、框都是插件画的,所以接哪家 AI 都一样好看,也不会收 AI 写的 SVG 代码。
|
||||
|
||||
|
||||
@@ -172,6 +172,22 @@ class AnswerTests(unittest.TestCase):
|
||||
self.assertEqual(ai_panels.get_answers("Claude")["answers"][0]["text"], "背景再亮一点")
|
||||
self.assertEqual(ai_panels.get_answers("Cursor")["answers"], [], "another app's panels are not its")
|
||||
|
||||
def test_a_skipped_question_and_when_the_ai_got_each_answer(self):
|
||||
shown = ai_panels.show("A", "细节", [CHOICES, {"type": "input", "id": "more", "question": "补充细节(可选)"}])
|
||||
task = ai_tasks.list_tasks()[0]["id"]
|
||||
step = ai_tasks.get_task(task)["steps"][0]["id"]
|
||||
ai_panels.answer(task, step, "pick", ["a"])
|
||||
ai_panels.answer(task, step, "more", skip=True)
|
||||
self.assertEqual(ai_tasks.list_tasks()[0]["asks"], 0, "a skipped question waits no more")
|
||||
got = ai_panels.get_answers("A", shown["id"])
|
||||
self.assertEqual(got["answers"][1], {"panel": shown["id"], "question": "more", "asked": "补充细节(可选)", "skipped": True})
|
||||
answers = ai_tasks.get_task(task)["steps"][0]["answers"]
|
||||
self.assertTrue(all(given["delivered"] and given["delivered_at"] >= given["at"] for given in answers.values()),
|
||||
"the page shows when the AI got each answer")
|
||||
self.assertTrue(ai_panels.leave_message(task, "继续")["id"])
|
||||
ai_panels.get_answers("A")
|
||||
self.assertTrue(ai_tasks.get_task(task)["steps"][-1]["delivered_at"])
|
||||
|
||||
def test_waiting_for_an_answer(self):
|
||||
shown = ai_panels.show("A", "等你", [CHOICES])
|
||||
task = ai_tasks.list_tasks()[0]
|
||||
@@ -376,6 +392,12 @@ class AskUserTests(unittest.TestCase):
|
||||
self.assertEqual((step["panel"]["title"], block["options"][0].get("recommended"), block["other"]), ("先测哪种双人题材?", True, True))
|
||||
self.assertEqual(step["tool"], "ask_user", "the timeline says the AI asked")
|
||||
|
||||
def test_a_skipped_question_says_so(self):
|
||||
self.answer_later(skip=True)
|
||||
got = mcp_task_tools.ask_user(self.ctx, "要不要加光效?", ["要", "不要"], wait_seconds=5)
|
||||
self.assertEqual((got["answered"], got["skipped"]), (True, True))
|
||||
self.assertIn("own judgement", got["note"])
|
||||
|
||||
def test_their_own_answer_when_no_option_fits(self):
|
||||
self.answer_later(text="冰与火,但要室内")
|
||||
got = mcp_task_tools.ask_user(self.ctx, "选哪个?", ["甲", "乙"], wait_seconds=5)
|
||||
|
||||
+16
-16
@@ -1376,8 +1376,6 @@ export const i18n = {
|
||||
aiMarkRemove: "删掉这个标记",
|
||||
aiMarkNeedOne: "先在图上画一个标记。",
|
||||
aiMarkNoNote: "(没写说明)",
|
||||
aiMarksSeen: "AI 已经收到",
|
||||
aiMarksWaiting: "AI 下一步会收到",
|
||||
aiTool_user_marks: "你在图上标了要改的地方",
|
||||
aiRunQueued: "排队中:前面还有 {ahead} 个",
|
||||
aiRunPrompt: "第 {at}/{count} 次",
|
||||
@@ -1458,7 +1456,7 @@ export const i18n = {
|
||||
aiStage_library: "模型库",
|
||||
aiTool_user_activity: "你在 AMB 里改了",
|
||||
aiChangeSeen: "AI 已经知道",
|
||||
aiChangeWaiting: "AI 做下一步时会知道",
|
||||
aiChangeWaiting: "已记下 · AI 做下一步时会知道",
|
||||
aiTickCompare: "勾上,挑两张对比参数",
|
||||
aiTickClear: "取消勾选",
|
||||
aiTickedOne: "已勾 1 张,再勾 1 张就能对比参数",
|
||||
@@ -1531,8 +1529,6 @@ export const i18n = {
|
||||
aiMessageSend: "发送",
|
||||
aiMessageHint: "AI 做下一步或来读取时就会收到。它要是已经停下了,请回到 AI 软件里说。",
|
||||
aiMessageEnded: "这个任务已经结束了,AI 多半不会再来看;新的要求请回到 AI 软件里说。",
|
||||
aiMessageSeen: "AI 已经看到",
|
||||
aiMessageWaiting: "等 AI 来取",
|
||||
aiMarkZoom: "放大看这里(再点一下还原)",
|
||||
aiMarkPlace: "这里",
|
||||
aiSliderHint: "左右拖动对比",
|
||||
@@ -1556,9 +1552,13 @@ export const i18n = {
|
||||
aiPanelRecommended: "推荐",
|
||||
aiPanelOther: "都不是?写下你的想法",
|
||||
aiTool_ask_user: "问了你",
|
||||
aiPanelHanded: "已经交出去了,AI 做下一步或来读取时就会看到。",
|
||||
aiPanelSeen: "AI 已经看到了你的回答。",
|
||||
aiPanelHow: "你的回答会留在这里,AI 做下一步或来读取时就会拿到。",
|
||||
aiPanelSeen: "AI 已收到你的回答",
|
||||
aiPanelSkip: "这题跳过",
|
||||
aiPanelSkipped: "你跳过了这题",
|
||||
aiDeliverySeen: "AI 已收到",
|
||||
aiDeliveryWaiting: "已发送 · 等 AI 来取",
|
||||
aiDeliveryWaitingTip: "已经存好了,AI 做下一步或来读取时就会拿到,拿到后这里会变成“AI 已收到”。AI 要是已经停下了,到 AI 软件里跟它说一声“看一下面板”。",
|
||||
aiPanelHow: "每题答完会显示“已发送”,AI 拿到后变成绿色的“AI 已收到”。不想答的题可以点“这题跳过”。",
|
||||
aiPanelClosed: "AI 已经关掉了这个面板,不用再回答了。",
|
||||
aiToastAsks: "AI 在问你:",
|
||||
aiToastShows: "AI 给你看:",
|
||||
@@ -4518,8 +4518,6 @@ export const i18n = {
|
||||
aiMarkRemove: "Remove this mark",
|
||||
aiMarkNeedOne: "Draw a mark on the picture first.",
|
||||
aiMarkNoNote: "(no note)",
|
||||
aiMarksSeen: "The AI has them",
|
||||
aiMarksWaiting: "The AI gets them with its next step",
|
||||
aiTool_user_marks: "You marked what to change",
|
||||
aiRunQueued: "Queued: {ahead} ahead",
|
||||
aiRunPrompt: "run {at} of {count}",
|
||||
@@ -4600,7 +4598,7 @@ export const i18n = {
|
||||
aiStage_library: "Library",
|
||||
aiTool_user_activity: "You changed in Anomalous",
|
||||
aiChangeSeen: "The AI knows",
|
||||
aiChangeWaiting: "The AI hears of it with its next step",
|
||||
aiChangeWaiting: "Noted · the AI hears of it with its next step",
|
||||
aiTickCompare: "Tick two to compare their settings",
|
||||
aiTickClear: "Untick all",
|
||||
aiTickedOne: "1 picture ticked: tick one more to compare settings",
|
||||
@@ -4673,8 +4671,6 @@ export const i18n = {
|
||||
aiMessageSend: "Send",
|
||||
aiMessageHint: "The AI gets it on its next step or when it asks. If it has stopped, tell it in the AI app.",
|
||||
aiMessageEnded: "This task has ended, so the AI will probably not look here again; ask for more in the AI app.",
|
||||
aiMessageSeen: "The AI has seen it",
|
||||
aiMessageWaiting: "Waiting for the AI to pick it up",
|
||||
aiMarkZoom: "Zoom in here (again: back)",
|
||||
aiMarkPlace: "Here",
|
||||
aiSliderHint: "Drag to compare",
|
||||
@@ -4698,9 +4694,13 @@ export const i18n = {
|
||||
aiPanelRecommended: "Suggested",
|
||||
aiPanelOther: "None of these? Write your own",
|
||||
aiTool_ask_user: "Asked you",
|
||||
aiPanelHanded: "Handed over: the AI sees it on its next step.",
|
||||
aiPanelSeen: "The AI has seen your answer.",
|
||||
aiPanelHow: "Your answer waits here until the AI's next step or check.",
|
||||
aiPanelSeen: "The AI has your answer",
|
||||
aiPanelSkip: "Skip this one",
|
||||
aiPanelSkipped: "You skipped this one",
|
||||
aiDeliverySeen: "The AI has it",
|
||||
aiDeliveryWaiting: "Sent · waiting for the AI",
|
||||
aiDeliveryWaitingTip: "Saved: the AI gets it with its next step or check, and this turns to \"The AI has it\". If the AI has stopped, tell it in the AI app to look at the panel.",
|
||||
aiPanelHow: "Each answer shows \"Sent\", then turns green when the AI has it. Skip any question you would rather not answer.",
|
||||
aiPanelClosed: "The AI closed this panel; no answer is needed any more.",
|
||||
aiToastAsks: "The AI asks you:",
|
||||
aiToastShows: "The AI shows you:",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Whether the AI has what the user gave it (an answer on a panel, a message, marks, pictures, a
|
||||
* stop, a change): a pill that says "sent, waiting for the AI" until the AI's next step or check
|
||||
* takes it, then "the AI has it" with the time it did (`delivered_at`, from ai_panels._collect).
|
||||
*/
|
||||
|
||||
import { translate as t } from './locales.js';
|
||||
import { timeLabel } from './activity_log.js';
|
||||
import { icon } from './ui_ai_icons.js';
|
||||
|
||||
/** The pill for `item` ({delivered, delivered_at}); `seen` / `waiting` are locale keys. */
|
||||
export function deliveryState(item, seen = 'aiDeliverySeen', waiting = 'aiDeliveryWaiting') {
|
||||
const got = Boolean(item?.delivered);
|
||||
const pill = document.createElement('div');
|
||||
pill.className = `anomalous-ai-delivery ${got ? 'is-seen' : 'is-waiting'}`;
|
||||
pill.setAttribute('role', 'status');
|
||||
const words = document.createElement('span');
|
||||
words.textContent = got && item.delivered_at ? `${t(seen)} · ${timeLabel(item.delivered_at)}` : t(got ? seen : waiting);
|
||||
pill.append(icon(got ? 'checks' : 'check', 13), words);
|
||||
if (!got) pill.title = t('aiDeliveryWaitingTip');
|
||||
return pill;
|
||||
}
|
||||
@@ -14,6 +14,7 @@ const PATHS = {
|
||||
star: '<path d="m12 3.3 2.6 5.5 6 .8-4.4 4.1 1.1 5.9L12 16.8l-5.3 2.8 1.1-5.9-4.4-4.1 6-.8z"/>',
|
||||
starOn: '<path d="m12 3.3 2.6 5.5 6 .8-4.4 4.1 1.1 5.9L12 16.8l-5.3 2.8 1.1-5.9-4.4-4.1 6-.8z" fill="currentColor"/>',
|
||||
check: '<path d="m5 12.5 4.3 4.3L19 7.2"/>',
|
||||
checks: '<path d="m2.5 12.5 4.3 4.3L16.5 7.2"/><path d="m11.3 16.6.2.2 10-9.6"/>',
|
||||
plus: '<path d="M12 5v14M5 12h14"/>',
|
||||
close: '<path d="M6 6l12 12M18 6 6 18"/>',
|
||||
sparkle: '<path d="M12 3c.6 4.6 2.4 6.4 7 7-4.6.6-6.4 2.4-7 7-.6-4.6-2.4-6.4-7-7 4.6-.6 6.4-2.4 7-7z"/><path d="M19 15.5c.25 1.7.8 2.25 2.5 2.5-1.7.25-2.25.8-2.5 2.5-.25-1.7-.8-2.25-2.5-2.5 1.7-.25 2.25-.8 2.5-2.5z"/>',
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { translate as t } from './locales.js';
|
||||
import { anomalousAlert } from './ui_dialog.js';
|
||||
import { outputUrl } from './ai_task_view.js';
|
||||
import { deliveryState } from './ui_ai_delivery.js';
|
||||
|
||||
const TOOLS = [['box', '□'], ['circle', '○'], ['arrow', '↗'], ['pen', '✎'], ['mask', '▧']];
|
||||
const MASK_COLOR = 'rgba(255, 64, 160, 0.45)';
|
||||
@@ -367,6 +368,6 @@ export function marksStep(step, taskId, owner) {
|
||||
const words = box.lastChild;
|
||||
if (step.note) words.append(el('div', 'anomalous-ai-user-text', step.note));
|
||||
if (step.inpaint) words.append(el('div', 'anomalous-ai-marks-inpaint', t('aiMarkInpaintReady', { file: step.inpaint })));
|
||||
words.append(notes, el('div', 'anomalous-ai-user-state', t(step.delivered ? 'aiMarksSeen' : 'aiMarksWaiting')));
|
||||
words.append(notes, deliveryState(step));
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* text, pictures (a row, a grid or side by side to compare, with marks), the slider, picks and
|
||||
* the diagram (ui_ai_stage.js), the user's models as cards with their covers, tables, lists,
|
||||
* settings, a progress bar, and questions: buttons (one pick or several), picks and a line of
|
||||
* text, whose answers go back to the server for the AI to read. Only
|
||||
* text, whose answers (or a skip) go back to the server for the AI to read, each with a pill
|
||||
* saying whether the AI has it yet (ui_ai_delivery.js). Only
|
||||
* text is ever written into the page (textContent); the blocks were checked by the server and
|
||||
* are checked again by their shape here. Also the toast when the AI shows a panel while the
|
||||
* user is elsewhere.
|
||||
@@ -14,6 +15,7 @@ import { outputUrl, isVideo } from './ai_task_view.js';
|
||||
import { addMarks, diagram, picks, slider } from './ui_ai_stage.js';
|
||||
import { runMapBox } from './ui_ai_run_map.js';
|
||||
import { decoratePicture } from './ui_ai_images.js';
|
||||
import { deliveryState } from './ui_ai_delivery.js';
|
||||
|
||||
function el(tag, className, text) {
|
||||
const node = document.createElement(tag);
|
||||
@@ -41,9 +43,27 @@ async function send(taskId, body) {
|
||||
return data;
|
||||
}
|
||||
|
||||
/** What happens to an answer: waiting for the AI to read it, or read. */
|
||||
/** What became of an answer: skipped (said so), and whether the AI has it yet. */
|
||||
function answerNote(answer) {
|
||||
return el('div', 'anomalous-ai-panel-answer-note', t(answer.delivered ? 'aiPanelSeen' : 'aiPanelHanded'));
|
||||
const box = el('div', 'anomalous-ai-panel-answer');
|
||||
if (answer.skipped) box.append(el('div', 'anomalous-ai-panel-typed is-skipped', t('aiPanelSkipped')));
|
||||
box.append(deliveryState(answer, 'aiPanelSeen'));
|
||||
return box;
|
||||
}
|
||||
|
||||
/** Leaves a question unanswered on purpose; the AI hears that it was skipped. */
|
||||
function skipButton(ctx, block, note) {
|
||||
const node = button('anomalous-ai-panel-skip', t('aiPanelSkip'), async () => {
|
||||
node.disabled = true;
|
||||
try {
|
||||
await send(ctx.taskId, { step: ctx.stepId, block: block.id, skip: true });
|
||||
ctx.changed();
|
||||
} catch (error) {
|
||||
note.textContent = error.message;
|
||||
node.disabled = false;
|
||||
}
|
||||
});
|
||||
return node;
|
||||
}
|
||||
|
||||
function picture(item, ctx, className) {
|
||||
@@ -174,6 +194,7 @@ function choices(block, ctx) {
|
||||
if (block.question) box.append(el('div', 'anomalous-ai-panel-question', block.question));
|
||||
const answer = ctx.answers[block.id];
|
||||
const options = el('div', 'anomalous-ai-panel-options');
|
||||
const note = el('div', 'anomalous-ai-panel-error');
|
||||
const picked = new Set(answer?.choice || []);
|
||||
const submit = async (ids) => {
|
||||
options.querySelectorAll('button').forEach(node => { node.disabled = true; });
|
||||
@@ -210,7 +231,7 @@ function choices(block, ctx) {
|
||||
if (block.multiple && !answer && !ctx.closed) box.append(confirm);
|
||||
if (answer?.text) box.append(el('div', 'anomalous-ai-panel-typed', answer.text));
|
||||
else if (block.other && !answer && !ctx.closed) box.append(otherLine(ctx, block, options));
|
||||
const note = el('div', 'anomalous-ai-panel-error');
|
||||
if (!answer && !ctx.closed) box.append(skipButton(ctx, block, note));
|
||||
box.append(note);
|
||||
if (answer) box.append(answerNote(answer));
|
||||
return box;
|
||||
@@ -221,7 +242,8 @@ function input(block, ctx) {
|
||||
box.append(el('div', 'anomalous-ai-panel-question', block.question));
|
||||
const answer = ctx.answers[block.id];
|
||||
if (answer) {
|
||||
box.append(el('div', 'anomalous-ai-panel-typed', answer.text), answerNote(answer));
|
||||
if (!answer.skipped) box.append(el('div', 'anomalous-ai-panel-typed', answer.text));
|
||||
box.append(answerNote(answer));
|
||||
return box;
|
||||
}
|
||||
const line = el('div', 'anomalous-ai-panel-input');
|
||||
@@ -248,7 +270,9 @@ function input(block, ctx) {
|
||||
if (event.key === 'Enter') go.click();
|
||||
});
|
||||
line.append(field, go);
|
||||
box.append(line, note);
|
||||
box.append(line);
|
||||
if (!ctx.closed) box.append(skipButton(ctx, block, note));
|
||||
box.append(note);
|
||||
return box;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { KINDS, keptCatalog, loadCatalog, nodeItems, nodeSources, prefetchToolbo
|
||||
import { card, coverUrl, detail, refreshCard } from './ui_ai_toolbox_cards.js';
|
||||
import { icon, iconButton } from './ui_ai_icons.js';
|
||||
import { emptyPicture, sourceBadge } from './ui_ai_toolbox_icons.js';
|
||||
import { deliveryState } from './ui_ai_delivery.js';
|
||||
|
||||
const MODES = ['prefer', 'only'];
|
||||
const CHUNK = 60;
|
||||
@@ -476,6 +477,6 @@ export function toolboxStep(step) {
|
||||
}
|
||||
box.append(mode, chips);
|
||||
}
|
||||
box.append(el('div', 'anomalous-ai-user-state', t(step.delivered ? 'aiMessageSeen' : 'aiMessageWaiting')));
|
||||
box.append(deliveryState(step));
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import { toolboxButton, toolboxStep } from './ui_ai_toolbox.js';
|
||||
import { executionStatus } from './ui_ai_status.js';
|
||||
import { canvasControls } from './ui_ai_task_canvas.js';
|
||||
import { canvasChanges } from './ui_canvas_changes.js';
|
||||
import { deliveryState } from './ui_ai_delivery.js';
|
||||
import {
|
||||
activityIds, duration, groupSteps, lookTools, stepDetail, taskCounts, taskResults, taskState, toolKey,
|
||||
} from './ai_task_view.js';
|
||||
@@ -140,13 +141,11 @@ function stepRow(step, number) {
|
||||
body.append(userChange(step));
|
||||
} else if (step.tool === 'user_pictures') {
|
||||
body.append(el('div', 'anomalous-ai-user-text', step.text || t('aiPicturesStep', { count: (step.results || []).length })),
|
||||
el('div', 'anomalous-ai-user-state', t(step.delivered ? 'aiPicturesSeen' : 'aiMessageWaiting')));
|
||||
deliveryState(step, 'aiPicturesSeen'));
|
||||
} else if (step.tool === 'user_stop') {
|
||||
body.append(el('div', 'anomalous-ai-user-text', t('aiRunStoppedByYou')),
|
||||
el('div', 'anomalous-ai-user-state', t(step.delivered ? 'aiMessageSeen' : 'aiMessageWaiting')));
|
||||
body.append(el('div', 'anomalous-ai-user-text', t('aiRunStoppedByYou')), deliveryState(step));
|
||||
} else if (step.kind === 'user') {
|
||||
body.append(el('div', 'anomalous-ai-user-text', step.text),
|
||||
el('div', 'anomalous-ai-user-state', t(step.delivered ? 'aiMessageSeen' : 'aiMessageWaiting')));
|
||||
body.append(el('div', 'anomalous-ai-user-text', step.text), deliveryState(step));
|
||||
} else if (step.text) {
|
||||
body.append(noteNode(step.text));
|
||||
}
|
||||
@@ -182,7 +181,7 @@ function userChange(step) {
|
||||
const note = el('span', 'anomalous-ai-link-note');
|
||||
box.append(canvasChanges(entry, { locate: change => locateNode(changeNodeId(change), note) }), note);
|
||||
}
|
||||
box.append(el('div', 'anomalous-ai-user-state', t(step.delivered ? 'aiChangeSeen' : 'aiChangeWaiting')));
|
||||
box.append(deliveryState(step, 'aiChangeSeen', 'aiChangeWaiting'));
|
||||
return box;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -35,8 +35,8 @@
|
||||
@import url("./styles/34-voice-over-panel.css?v=20261021-sample");
|
||||
@import url("./styles/35-voice-over-drafts.css?v=20261020-drafts");
|
||||
@import url("./styles/36-reference-audio.css?v=20261020-remove");
|
||||
@import url("./styles/37-ai-workbench.css?v=20261024-canvas");
|
||||
@import url("./styles/38-ai-panels.css?v=20261023-ask");
|
||||
@import url("./styles/37-ai-workbench.css?v=20261024-delivery");
|
||||
@import url("./styles/38-ai-panels.css?v=20261024-delivery");
|
||||
@import url("./styles/39-ai-stage.css?v=20261021-stage");
|
||||
@import url("./styles/40-ai-side.css?v=20261023-look");
|
||||
@import url("./styles/41-ai-results.css?v=20261023-tags");
|
||||
|
||||
@@ -422,7 +422,6 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.anomalous-ai-user-state,
|
||||
.anomalous-ai-message-hint {
|
||||
color: var(--amb-text-dim);
|
||||
font-size: 11.5px;
|
||||
|
||||
@@ -418,12 +418,46 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.anomalous-ai-panel-answer-note,
|
||||
.anomalous-ai-panel-foot {
|
||||
color: var(--amb-text-dim);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.anomalous-ai-panel-answer { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
|
||||
.anomalous-ai-panel-typed.is-skipped { border-left-color: var(--amb-text-dim); background: rgba(var(--amb-ink-rgb), 0.05); color: var(--amb-text-soft); }
|
||||
|
||||
.anomalous-ai-panel-skip {
|
||||
align-self: flex-start;
|
||||
padding: 2px 4px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--amb-text-dim);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
text-decoration: underline dotted;
|
||||
cursor: pointer;
|
||||
}
|
||||
.anomalous-ai-panel-skip:hover { color: var(--amb-text-main); }
|
||||
|
||||
/* Whether the AI has what the user gave it (ui_ai_delivery.js): sent and waiting, or the AI has it. */
|
||||
.anomalous-ai-delivery {
|
||||
display: inline-flex;
|
||||
align-self: flex-start;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
width: fit-content;
|
||||
padding: 2px 10px 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
animation: anomalous-ai-delivery-in 0.35s ease-out;
|
||||
}
|
||||
.anomalous-ai-delivery.is-waiting { background: color-mix(in srgb, var(--amb-warn) 16%, transparent); color: var(--amb-warn-text); cursor: help; }
|
||||
.anomalous-ai-delivery.is-seen { background: color-mix(in srgb, var(--amb-ok) 16%, transparent); color: var(--amb-ok-text, var(--amb-ok)); }
|
||||
@keyframes anomalous-ai-delivery-in { from { opacity: 0; transform: translateY(3px); } }
|
||||
@media (prefers-reduced-motion: reduce) { .anomalous-ai-delivery { animation: none; } }
|
||||
|
||||
.anomalous-ai-panel-error {
|
||||
color: var(--amb-danger-text);
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user