The AI asks in Anomalous: ask_user, with a suggested option and a line of the user's own
ask_user shows a question in the AI workbench (options with an optional
recommended one, and unless other=false a line for an answer of the
user's own; no options: a line) and waits in the same call for the
answer, returning the choice or the text. Choices blocks gain
recommended and other; the step is named ask_user ("Asked you"). The
server instructions, begin_task and the workbench prompt tell the AI to
ask every question this way, never in its own chat.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
355f95af48
commit
52bd4cbce2
@@ -250,6 +250,10 @@ covered by the `styles.css` manifest.
|
||||
do, the sentence to send, where the steps will show). The pictures (`web/assets/ai_*.webp`)
|
||||
were made with ChatGPT. `ai_task_view.js` holds
|
||||
its rules without DOM (folding reads, tool words, counts, result addresses).
|
||||
`ask_user` (`api/mcp_task_tools.py`) is the AI's question in the workbench: a one-block panel
|
||||
(choices with an optional `recommended` option and an `other` line, or an input) whose answer
|
||||
it waits for in the same call; the server instructions and `begin_task` tell the AI to ask
|
||||
this way rather than in its own chat.
|
||||
Panels the AI draws (`show_panel`, `update_panel`): `api/ai_panels.py` checks every block
|
||||
against a fixed set (heading, text, images from the output folder, models by id with their
|
||||
covers, table, list, params, choices, input, progress, divider; plain strings of bounded
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
- AI 可以在你眼前的画布上搭工作流了:查这台 ComfyUI 有哪些节点(包括自定义节点)、每个节点有哪些输入输出和可选项;新建一个工作流标签页(空白的,或者一次搭好整套并自动排好位置,不动你正在开的那个);加节点、连线、改参数、删节点。每一步都在画布上当场发生,是一次 Ctrl+Z,记在操作记录里,并在 AI 工作台那一步下面显示改了什么,连线也会写成“某节点的输入 model:(空)→ 加载模型 #4 · MODEL”。连不上的类型、不存在的选项、写错的参数会先被拦下并告诉 AI 正确的写法,画布不会留下半截。新建的标签页名字带时间,不会打开或覆盖你存过的工作流。连线会绕成死循环时也会先拦下。运行被 ComfyUI 拒绝时,AI 收到的是 ComfyUI 给的具体原因(哪个节点、什么问题),不再只有一句“运行失败”。
|
||||
- The AI can build workflows on the canvas the user sees: look up this ComfyUI's node types (custom nodes too) with their inputs, outputs and choices; open a new tab, blank or built and laid out from a workflow in API form (the open one is left alone); add, wire, set and remove nodes. Each is one Ctrl+Z step, logged as the AI's and shown under its AI workbench step, rewiring included. Wrong types, choices and settings are refused with what fits, before anything changes. Wiring that would loop is refused too, and a run ComfyUI rejects reports ComfyUI's own reason.
|
||||
|
||||
### AI 的问题在 AMB 里问 / The AI asks in Anomalous
|
||||
- AI 接了 AMB 的任务以后,要你做选择(比如“先测哪种题材?”)不再弹在 AI 软件的对话框里,而是出现在 AI 工作台(展台上有提示,你不在工作台时右下角也会提醒):几个选项按钮,AI 推荐的那个标着“推荐”,每个选项可以带一句说明;下面还有一行“都不是?写下你的想法”。你点了或写了,AI 在同一步里马上拿到答案,接着干活。AMB 告诉 AI 的说明里写明了:干 AMB 的活时,问题都这样问。
|
||||
- New `ask_user` tool: a question with options (one may be marked recommended) and a line for an answer of the user's own, shown in the AI workbench; the AI waits for the answer in the same call. The server instructions tell AIs to ask this way, not in their own chat.
|
||||
|
||||
### AI 工作台换新外观 / A new look for the AI workbench
|
||||
- 左边任务栏:“新任务”成了醒目的主按钮,“连接 AI”和“关卡”变成带图标的按钮;下面写着最近接入的是哪个 AI、什么时候;有任务在进行、等你回答或出错时,上面出现对应的筛选按钮和数量;每个任务前的状态换成清楚的图标,选中的任务左边有一条色条。
|
||||
- 任务标题栏:状态标签带图标;“素材”“全部撤销”“显示展台”是统一样式的按钮,“改名”和“从列表移除”换成铅笔和垃圾桶图标(鼠标移上去有说明,删除悬停时变红)。
|
||||
|
||||
@@ -57,6 +57,7 @@ TOOLS = {
|
||||
# the AI's own: notes and panels are steps; the rest only steer the task
|
||||
"report": Entry("note", None, None),
|
||||
"show_panel": Entry("panel", None, None),
|
||||
"ask_user": Entry("panel", None, None),
|
||||
"begin_task": Entry(None, None, None),
|
||||
"update_panel": Entry(None, None, None),
|
||||
"get_answers": Entry(None, None, None),
|
||||
|
||||
+10
-6
@@ -233,9 +233,11 @@ def clean_block(block, at, taken):
|
||||
if option_id in ids:
|
||||
raise PanelError(f"{where}.options: id {option_id!r} twice")
|
||||
ids.add(option_id)
|
||||
options.append({"id": option_id, "label": label, "detail": _text(option.get("detail"), 200, f"{where}.options[{i}].detail")})
|
||||
options.append({"id": option_id, "label": label, "detail": _text(option.get("detail"), 200, f"{where}.options[{i}].detail"),
|
||||
**({"recommended": True} if option.get("recommended") is True else {})})
|
||||
# other: a line for an answer none of the options is
|
||||
out.update(question=_text(block.get("question"), 200, f"{where}.question"), options=options,
|
||||
multiple=block.get("multiple") is True)
|
||||
multiple=block.get("multiple") is True, other=block.get("other") is True)
|
||||
elif kind == "input":
|
||||
out.update(question=_text(block.get("question"), 200, f"{where}.question", True),
|
||||
placeholder=_text(block.get("placeholder"), 80, f"{where}.placeholder"))
|
||||
@@ -322,12 +324,12 @@ def _find(panel_id):
|
||||
return None, None
|
||||
|
||||
|
||||
def show(client, title, blocks, now=None, session="", task_id=None):
|
||||
def show(client, title, blocks, now=None, session="", task_id=None, tool="show_panel"):
|
||||
title = _text(title, 120, "title", True)
|
||||
panel = {"id": uuid.uuid4().hex[:10], "title": title, "blocks": _block_ids(clean_blocks(blocks)), "closed": False}
|
||||
now = time.time() if now is None else now
|
||||
step = {"id": uuid.uuid4().hex[:10], "at": now, "ms": 0, "tool": "show_panel", "kind": "panel", "ok": True,
|
||||
"summary": f"show_panel: {title}", "panel": panel, "answers": {}, "results": []}
|
||||
step = {"id": uuid.uuid4().hex[:10], "at": now, "ms": 0, "tool": tool, "kind": "panel", "ok": True,
|
||||
"summary": f"{tool}: {title}", "panel": panel, "answers": {}, "results": []}
|
||||
with ai_tasks._lock:
|
||||
task = ai_tasks.current_task(client, now, session=session, task_id=task_id)
|
||||
ai_tasks._add_step(task, step)
|
||||
@@ -371,7 +373,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"] in ("choices", "picks"):
|
||||
if 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]
|
||||
options = block["options"] if block["type"] == "choices" else block["items"]
|
||||
known = {option["id"]: option.get("label") or option["id"] for option in options}
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ RUN_WATCH_SECONDS = 30 * 60
|
||||
MEDIA_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".mp4", ".webm"}
|
||||
|
||||
# What each tool is (its kind, stage and gate) is action_catalog.py's.
|
||||
OWN_STEPS = {"report", "show_panel"} # make their step themselves (with its text or panel)
|
||||
OWN_STEPS = {"report", "show_panel", "ask_user"} # make their step themselves (with its text or panel)
|
||||
|
||||
_lock = threading.RLock()
|
||||
_tasks = None # id -> task
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ from .mcp_tools import ToolError, _image_content, _resolve_image, _schema
|
||||
EXTENSION = "io.modelcontextprotocol/ui"
|
||||
MIME = "text/html;profile=mcp-app"
|
||||
CARD_URI = "ui://anomalous/task-card.html"
|
||||
CARD_TOOLS = ("show_panel", "report", "run_workflow", "begin_task")
|
||||
CARD_TOOLS = ("show_panel", "ask_user", "report", "run_workflow", "begin_task")
|
||||
MAX_PICTURES = 6
|
||||
MAX_STEPS = 8
|
||||
HTML = os.path.join(os.path.dirname(__file__), "mcp_app", "task_card.html")
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@ PROMPTS = {
|
||||
"arguments": [{"name": "what", "description": "What to do", "required": True}],
|
||||
"text": ("Use Anomalous for this: {what}\n"
|
||||
"Call begin_task with a short title in my language first. Show your plan, then results, with "
|
||||
"show_panel in the order they happen; when a choice is mine, ask with choices or picks and wait "
|
||||
"with get_answers. Honour any user_messages that come with a tool result. End with report(done=true)."),
|
||||
"show_panel in the order they happen; when a choice is mine, ask it with ask_user (or a picks panel, "
|
||||
"waiting with get_answers), never in this chat. Honour any user_messages that come with a tool result. End with report(done=true)."),
|
||||
},
|
||||
"pick_best": {
|
||||
"title": "Make a few, let me pick",
|
||||
|
||||
+3
-1
@@ -55,7 +55,9 @@ INSTRUCTIONS = (
|
||||
"new_task=true is only for a separate user request), and report to explain a choice, hand in "
|
||||
"pictures made another way, or end the task (done=true). The user can write to you or answer "
|
||||
"panel questions there: call get_answers to wait for or read them, and any unread user_messages "
|
||||
"or user_answers attached to a tool result must also be honoured. Show plans, workflow explanations "
|
||||
"or user_answers attached to a tool result must also be honoured. While you work on an Anomalous task, ask "
|
||||
"the user every question (a choice, a preference, yes or no) with ask_user, never in your own chat or its "
|
||||
"question form: the user watches and answers in Anomalous. Show plans, workflow explanations "
|
||||
"and results in chronological order with show_panel; update_panel appends new content below what "
|
||||
"the user already saw."
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ def begin_task(ctx, title, new_task=False):
|
||||
session=ctx.get("session") or "", task_id=ctx.get("task"),
|
||||
)
|
||||
answer = {"task": task["id"], "title": task["title"], "note": "Continue this task; do not begin another for its individual steps. "
|
||||
"Ask the user anything with ask_user: they answer in Anomalous's AI workbench, not in your chat. "
|
||||
"Every call from now on is a step of this task in Anomalous's AI workbench, "
|
||||
"where the user sees, opens and undoes them. Call report to explain a choice "
|
||||
"or to hand in pictures you made another way; report with done=true at the end."}
|
||||
@@ -64,6 +65,32 @@ def show_panel(ctx, title, blocks, task=None):
|
||||
return answer
|
||||
|
||||
|
||||
def ask_user(ctx, question, options=None, multiple=False, other=True, wait_seconds=ai_panels.MAX_WAIT, task=None):
|
||||
"""A question in the AI workbench, where the user watches the work: options to press (one may be
|
||||
recommended), a line of their own; waits for the answer."""
|
||||
block = ({"type": "choices", "id": "answer", "options": options, "multiple": bool(multiple), "other": bool(other)}
|
||||
if options else {"type": "input", "id": "answer", "question": str(question or "")[:200]})
|
||||
try:
|
||||
panel = ai_panels.show(ctx.get("client") or "AI", question, [block], session=ctx.get("session") or "",
|
||||
task_id=task or ctx.get("task"), tool="ask_user")
|
||||
answer = ai_panels.get_answers(ctx.get("client") or "AI", panel["id"], wait_seconds)
|
||||
except ai_panels.PanelError as error:
|
||||
raise ToolError(str(error))
|
||||
given = next((item for item in answer["answers"] if item["question"] == "answer"), None)
|
||||
if not given:
|
||||
return {"answered": False, "panel": panel["id"],
|
||||
"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"):
|
||||
out["text"] = given["text"]
|
||||
if given.get("choice") == ["other"]:
|
||||
out["note"] = "None of your options: the user wrote their own answer."
|
||||
else:
|
||||
out.update(choice=given.get("choice"), labels=given.get("labels"))
|
||||
return out
|
||||
|
||||
|
||||
def update_panel(ctx, panel, title=None, blocks=None, closed=None, task=None):
|
||||
del ctx, task
|
||||
try:
|
||||
@@ -181,6 +208,20 @@ TASK_TOOLS = dict([
|
||||
"blocks": {"type": "array", "items": {"type": "object"}, "maxItems": ai_panels.MAX_BLOCKS},
|
||||
"task": _TASK_PROP},
|
||||
["title", "blocks"]),
|
||||
_task_tool("ask_user", "Ask the user",
|
||||
"Asks the user in Anomalous's AI workbench, where they watch your work, and waits for the answer: "
|
||||
"a question with options to press (2-8 {label, detail, recommended}: mark the one you suggest with "
|
||||
"recommended=true instead of writing it into the label) and, unless other=false, a line for their own "
|
||||
"answer; no options: a question they answer in a line. Use it for every choice or preference that is "
|
||||
"the user's while you work on an Anomalous task, never your own chat's question form. Returns the "
|
||||
"choice (or their text); if they have not answered within wait_seconds, keep waiting with get_answers.",
|
||||
ask_user, {"question": {"type": "string"},
|
||||
"options": {"type": "array", "maxItems": 8, "items": {"type": "object", "properties": {
|
||||
"label": {"type": "string"}, "detail": {"type": "string"}, "recommended": {"type": "boolean"}}}},
|
||||
"multiple": {"type": "boolean", "default": False}, "other": {"type": "boolean", "default": True},
|
||||
"wait_seconds": {"type": "number", "minimum": 0, "maximum": ai_panels.MAX_WAIT, "default": ai_panels.MAX_WAIT},
|
||||
"task": _TASK_PROP},
|
||||
["question"]),
|
||||
_task_tool("update_panel", "Change a panel",
|
||||
"Adds new output at the BOTTOM of an existing panel, preserving earlier content and its order. "
|
||||
"Send only the new blocks; repeated unchanged blocks are kept once. To deliberately correct an "
|
||||
|
||||
@@ -108,6 +108,7 @@ claude mcp add --transport http anomalous http://127.0.0.1:8188/anomalous/mcp
|
||||
| download_status | 看下载进度和结果 |
|
||||
| list_voices / speak | 列出 Anomalous TTS 的角色;让某个角色读一段台词并保存(最多等两分钟) |
|
||||
| begin_task / report | 在 AI 工作台里起一个任务的标题;给你留言、交上它用别的方式做出来的图、结束任务(见第 5 节) |
|
||||
| ask_user | 在 AI 工作台里问你一个问题:几个选项(AI 推荐的那个标着“推荐”),加一行“都不是?写下你的想法”;AI 在同一步里等你回答 |
|
||||
| show_panel / update_panel / get_answers | 在 AI 工作台里画一个面板(对比图片、模型卡片、表格、按钮和输入框);改它或关掉它的问题;读你的回答(见第 5 节) |
|
||||
|
||||
## 5. AI 工作台:AI 做了什么,一目了然(1.62 起)
|
||||
|
||||
@@ -347,5 +347,56 @@ class AnswerTests(unittest.TestCase):
|
||||
self.assertEqual([m["text"] for m in answer["messages"]], ["one"], "legacy callers without routing still read their own input")
|
||||
|
||||
|
||||
class AskUserTests(unittest.TestCase):
|
||||
"""ask_user: the AI's question shows in the workbench and the answer comes back to the same call."""
|
||||
|
||||
def setUp(self):
|
||||
isolate()
|
||||
self.ctx = {"client": "Gemini", "session": "s1"}
|
||||
|
||||
def answer_later(self, **given):
|
||||
def go():
|
||||
for _ in range(100):
|
||||
tasks = ai_tasks.list_tasks()
|
||||
task = ai_tasks.get_task(tasks[0]["id"]) if tasks else None
|
||||
step = next((item for item in (task or {}).get("steps", []) if item.get("kind") == "panel"), None)
|
||||
if step:
|
||||
ai_panels.answer(task["id"], step["id"], "answer", **given)
|
||||
return
|
||||
time.sleep(0.02)
|
||||
threading.Thread(target=go, daemon=True).start()
|
||||
|
||||
def test_a_pick_comes_back_and_the_recommended_one_is_marked(self):
|
||||
options = [{"label": "光与暗双生少女", "detail": "高难度", "recommended": True}, {"label": "猫耳娘咖啡馆"}]
|
||||
self.answer_later(choice=["o2"])
|
||||
got = mcp_task_tools.ask_user(self.ctx, "先测哪种双人题材?", options, wait_seconds=5)
|
||||
self.assertEqual((got["answered"], got["labels"]), (True, ["猫耳娘咖啡馆"]))
|
||||
step = ai_tasks.get_task(ai_tasks.list_tasks()[0]["id"])["steps"][0]
|
||||
block = step["panel"]["blocks"][0]
|
||||
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_their_own_answer_when_no_option_fits(self):
|
||||
self.answer_later(text="冰与火,但要室内")
|
||||
got = mcp_task_tools.ask_user(self.ctx, "选哪个?", ["甲", "乙"], wait_seconds=5)
|
||||
self.assertEqual((got["text"], got["answered"]), ("冰与火,但要室内", True))
|
||||
self.assertIn("own answer", got["note"])
|
||||
|
||||
def test_no_options_is_a_line_and_no_answer_yet_says_how_to_keep_waiting(self):
|
||||
got = mcp_task_tools.ask_user(self.ctx, "想要几张?", wait_seconds=0)
|
||||
self.assertFalse(got["answered"])
|
||||
self.assertIn("get_answers", got["note"])
|
||||
step = ai_tasks.get_task(ai_tasks.list_tasks()[0]["id"])["steps"][0]
|
||||
self.assertEqual(step["panel"]["blocks"][0]["type"], "input")
|
||||
|
||||
def test_other_text_only_where_the_question_allows_it(self):
|
||||
panel = ai_panels.show("Gemini", "q", [{"type": "choices", "id": "answer", "options": ["a", "b"]}])
|
||||
task = ai_tasks.list_tasks()[0]["id"]
|
||||
step = ai_tasks.get_task(task)["steps"][0]["id"]
|
||||
with self.assertRaises(ai_panels.PanelError):
|
||||
ai_panels.answer(task, step, "answer", text="随便")
|
||||
self.assertTrue(panel["id"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1450,6 +1450,9 @@ export const i18n = {
|
||||
aiCountAsks: "{count} 个问题等你回答",
|
||||
aiPanelSend: "交给 AI",
|
||||
aiPanelTypeHere: "在这里打字",
|
||||
aiPanelRecommended: "推荐",
|
||||
aiPanelOther: "都不是?写下你的想法",
|
||||
aiTool_ask_user: "问了你",
|
||||
aiPanelHanded: "已经交出去了,AI 做下一步或来读取时就会看到。",
|
||||
aiPanelSeen: "AI 已经看到了你的回答。",
|
||||
aiPanelHow: "你的回答会留在这里,AI 做下一步或来读取时就会拿到。",
|
||||
@@ -4486,6 +4489,9 @@ export const i18n = {
|
||||
aiCountAsks: "{count} {count|question|questions} for you",
|
||||
aiPanelSend: "Hand it to the AI",
|
||||
aiPanelTypeHere: "Type here",
|
||||
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.",
|
||||
|
||||
@@ -138,6 +138,36 @@ function progress(block) {
|
||||
return box;
|
||||
}
|
||||
|
||||
/** An answer none of the options is: a line under them. */
|
||||
function otherLine(ctx, block, options) {
|
||||
const line = el('div', 'anomalous-ai-panel-input is-other');
|
||||
const field = el('input');
|
||||
field.type = 'text';
|
||||
field.maxLength = 2000;
|
||||
field.placeholder = t('aiPanelOther');
|
||||
const go = button('anomalous-ai-panel-send', t('aiPanelSend'), async () => {
|
||||
if (!field.value.trim()) return;
|
||||
go.disabled = true;
|
||||
options.querySelectorAll('button').forEach(node => { node.disabled = true; });
|
||||
try {
|
||||
await send(ctx.taskId, { step: ctx.stepId, block: block.id, text: field.value });
|
||||
ctx.changed();
|
||||
} catch (error) {
|
||||
go.disabled = false;
|
||||
options.querySelectorAll('button').forEach(node => { node.disabled = false; });
|
||||
field.setCustomValidity(error.message);
|
||||
field.reportValidity();
|
||||
}
|
||||
});
|
||||
field.addEventListener('keydown', (event) => {
|
||||
event.stopPropagation(); // typing here is not the browser's shortcuts
|
||||
if (event.key === 'Enter' && !event.isComposing) go.click();
|
||||
});
|
||||
field.addEventListener('input', () => field.setCustomValidity(''));
|
||||
line.append(field, go);
|
||||
return line;
|
||||
}
|
||||
|
||||
function choices(block, ctx) {
|
||||
const box = el('div', 'anomalous-ai-panel-ask');
|
||||
if (block.question) box.append(el('div', 'anomalous-ai-panel-question', block.question));
|
||||
@@ -162,6 +192,10 @@ function choices(block, ctx) {
|
||||
confirm.disabled = !options.querySelector('.anomalous-ai-panel-option.is-picked');
|
||||
});
|
||||
node.dataset.id = option.id;
|
||||
if (option.recommended) {
|
||||
node.classList.add('is-recommended');
|
||||
node.append(el('span', 'anomalous-ai-panel-option-badge', t('aiPanelRecommended')));
|
||||
}
|
||||
node.append(el('span', 'anomalous-ai-panel-option-label', option.label));
|
||||
if (option.detail) node.append(el('span', 'anomalous-ai-panel-option-detail', option.detail));
|
||||
node.disabled = Boolean(answer) || ctx.closed;
|
||||
@@ -173,6 +207,8 @@ function choices(block, ctx) {
|
||||
});
|
||||
confirm.disabled = true;
|
||||
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');
|
||||
box.append(note);
|
||||
if (answer) box.append(answerNote(answer));
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@
|
||||
@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=20261023-look");
|
||||
@import url("./styles/38-ai-panels.css?v=20261021-panels");
|
||||
@import url("./styles/38-ai-panels.css?v=20261023-ask");
|
||||
@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");
|
||||
|
||||
@@ -344,6 +344,25 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-ai-panel-option.is-recommended {
|
||||
border-color: color-mix(in srgb, var(--amb-link) 65%, transparent);
|
||||
background: color-mix(in srgb, var(--amb-link) 12%, transparent);
|
||||
}
|
||||
|
||||
.anomalous-ai-panel-option-badge {
|
||||
align-self: flex-start;
|
||||
margin-bottom: 2px;
|
||||
padding: 0 7px;
|
||||
border-radius: 999px;
|
||||
background: var(--amb-link);
|
||||
color: #fff;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.anomalous-ai-panel-input.is-other { margin-top: 10px; }
|
||||
|
||||
.anomalous-ai-panel-option-detail {
|
||||
color: var(--amb-text-soft);
|
||||
font-size: 11.5px;
|
||||
|
||||
Reference in New Issue
Block a user