feat: OpenMesh 基础平台与 MD/PDF 转换技能
Some checks failed
CI / pytest (push) Has been cancelled
CI / gui-unit (push) Has been cancelled
CI / gui-e2e (push) Has been cancelled

- 后端: coworker 智能体框架, WS API, 文件上传, 附件处理
- 前端: Open WebUI, 文件全量走 upload API (含 MD/TXT/JSON 等文本类)
- 技能: md-to-office (pandoc + wkhtmltopdf)
- 修复: 上传文件路径丢失, Agent 搜索浪费, 输出文件跑到 uploads/
- 打包: PyInstaller one-dir, 预打包 pandoc/wkhtmltopdf/chromium
This commit is contained in:
2026-09-13 23:41:04 +08:00
commit 6f402ffcee
638 changed files with 154534 additions and 0 deletions

View File

@@ -0,0 +1,636 @@
<!doctype html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenCoworker — Connectors redesign (v2, quiet)</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: ['selector', '[data-theme="dark"]'],
theme: { extend: {
colors: {
paper:'var(--paper)', panel:'var(--panel)', ink:'var(--ink)', muted:'var(--muted)',
faint:'var(--faint)', line:'var(--line)', lineStrong:'var(--line-strong)',
accent:'var(--accent)', accentSoft:'var(--accent-soft)', ok:'var(--ok)',
warnInk:'var(--warn-ink)', warnSoft:'var(--warn-soft)', danger:'var(--danger)',
},
fontFamily: { sans:['-apple-system','BlinkMacSystemFont','Segoe UI','Inter','system-ui','sans-serif'],
mono:['SF Mono','Menlo','monospace'] },
}},
};
</script>
<style>
:root{
--paper:#f5f5f7; --panel:#ffffff; --ink:#1d1d1f; --muted:#6e6e73; --faint:#aeaeb2;
--line:#e8e8ed; --line-strong:#d2d2d7; --accent:#2563eb; --accent-soft:#e9f0fd;
--ok:#34a853; --ok-dot:#34c759; --warn-ink:#b45309; --warn-soft:#fef3c7; --danger:#ff3b30;
}
html[data-theme="dark"]{
--paper:#1c1c1e; --panel:#2c2c2e; --ink:#f5f5f7; --muted:#98989d; --faint:#636366;
--line:#3a3a3c; --line-strong:#48484a; --accent:#4c8dff; --accent-soft:#1c2a44;
--ok:#30d158; --ok-dot:#30d158; --warn-ink:#ffd60a; --warn-soft:#3a2f0b; --danger:#ff453a;
}
body{ background:var(--paper); color:var(--ink); -webkit-font-smoothing:antialiased; }
[data-brand="slack"] { --brand:#611f69; --brand-soft:#f4ecf5; }
[data-brand="teams"] { --brand:#5059c9; --brand-soft:#eceefb; }
[data-brand="hubspot"] { --brand:#ff7a59; --brand-soft:#fff0ec; }
[data-brand="telegram"] { --brand:#229ed9; --brand-soft:#e6f4fb; }
[data-brand="gmail"] { --brand:#ea4335; --brand-soft:#fdecea; }
[data-brand="github"] { --brand:#1f2328; --brand-soft:#eef0f2; }
[data-brand="browser"] { --brand:#6b7280; --brand-soft:#eef0f2; }
[data-brand="linear"] { --brand:#5e6ad2; --brand-soft:#eef0fb; }
html[data-theme="dark"] [data-brand]{ --brand-soft:color-mix(in srgb, var(--brand) 18%, var(--panel)); }
html[data-theme="dark"] [data-brand="github"]{ --brand:#f5f5f7; }
/* macOS System-Settings grammar: grouped inset lists on gray paper */
.grp{ background:var(--panel); border-radius:12px; overflow:hidden;
box-shadow:0 0 0 0.5px var(--line-strong), 0 1px 2px rgba(0,0,0,.04); }
.row{ display:flex; align-items:center; gap:12px; padding:11px 16px; min-height:44px; }
.row + .row, .grp > * + *{ border-top:0.5px solid var(--line); }
.grp-h{ font-size:12px; font-weight:600; color:var(--muted); padding:0 16px; margin:22px 0 7px; }
.foot{ font-size:12px; color:var(--faint); padding:7px 16px 0; }
.badge{ width:34px; height:34px; border-radius:8px; background:var(--brand-soft); color:var(--brand);
display:grid; place-items:center; font-weight:700; font-size:15px; flex:none; }
.badge-sm{ width:24px; height:24px; border-radius:6px; background:var(--brand-soft); color:var(--brand);
display:grid; place-items:center; font-weight:700; font-size:12px; flex:none; }
.dot{ width:8px; height:8px; border-radius:9999px; flex:none; }
.pill{ font-size:12.5px; font-weight:500; padding:5px 12px; border-radius:9999px; flex:none; }
.pill-accent{ background:var(--accent); color:#fff; }
.pill-quiet{ background:color-mix(in srgb, var(--ink) 6%, transparent); color:var(--accent); }
.pill-line{ box-shadow:inset 0 0 0 1px var(--line-strong); color:var(--ink); }
.tag{ font-size:10.5px; font-weight:600; padding:2px 7px; border-radius:5px; flex:none; }
.tag-accent{ background:var(--accent-soft); color:var(--accent); }
.tag-warn{ background:var(--warn-soft); color:var(--warn-ink); }
.tag-quiet{ background:color-mix(in srgb, var(--ink) 5%, transparent); color:var(--muted); }
.chev{ color:var(--faint); font-size:15px; flex:none; }
details > summary{ list-style:none; cursor:pointer; }
details > summary::-webkit-details-marker{ display:none; }
details[open] .dchev{ transform:rotate(90deg); }
.dchev{ transition:transform .15s; display:inline-block; color:var(--faint); }
.x{ color:var(--faint); font-size:15px; line-height:1; } .x:hover{ color:var(--danger); }
input[type="checkbox"]{ accent-color:var(--accent); width:15px; height:15px; }
</style>
</head>
<body class="font-sans text-[13.5px]">
<!-- review-only switcher -->
<div class="flex items-center gap-3 px-4 h-11 bg-panel border-b border-line text-[12.5px]">
<span class="font-semibold tracking-tight">Connectors redesign · v2</span><span class="text-faint">static mock</span>
<div class="ml-2 inline-flex rounded-lg p-0.5 bg-paper">
<button data-view="list" class="view-tab px-3 py-1 rounded-md font-medium">Connectors</button>
<button data-view="slack" class="view-tab px-3 py-1 rounded-md font-medium">Slack</button>
<button data-view="gmail" class="view-tab px-3 py-1 rounded-md font-medium">Gmail</button>
<button data-view="hubspot" class="view-tab px-3 py-1 rounded-md font-medium">HubSpot</button>
<button data-view="teams" class="view-tab px-3 py-1 rounded-md font-medium">Teams</button>
</div>
<button id="themeBtn" class="ml-auto px-2.5 py-1 rounded-md bg-paper"></button>
</div>
<div class="flex" style="height:calc(100vh - 44px)">
<!-- app sidebar (abbreviated, real width) -->
<aside class="w-[264px] shrink-0 bg-panel border-r border-line flex flex-col">
<div class="px-3.5 pt-3.5 pb-2 flex items-center gap-2">
<div class="w-6 h-6 rounded-md bg-accent/15 grid place-items-center text-accent"></div>
<div class="font-semibold tracking-tight">OpenCoworker</div>
</div>
<div class="px-3 pb-2"><button class="w-full px-3 py-2 rounded-lg bg-accent text-white text-[13px] font-medium text-left"> New session</button></div>
<div class="flex-1 overflow-y-auto px-2.5 mt-2 space-y-0.5 text-[13px]">
<div class="px-1.5 text-[10.5px] uppercase tracking-[0.07em] text-faint font-semibold mb-1">Recent</div>
<button class="w-full px-2 py-2 rounded-lg hover:bg-paper text-left">hi<span class="block text-[11px] text-muted">Coworker</span></button>
<button class="w-full px-2 py-2 rounded-lg hover:bg-paper text-left">Q1 sales analysis<span class="block text-[11px] text-muted">Coworker</span></button>
</div>
<div class="px-2.5 py-2 border-t border-line">
<button class="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg bg-paper text-[13px] font-medium">⌘ Integrations</button>
</div>
</aside>
<!-- integrations sub-nav (no per-connector items) -->
<nav class="w-[208px] shrink-0 border-r border-line bg-panel/40 px-3 py-4 text-[13px]">
<div class="px-2 text-[13.5px] font-semibold mb-3">Integrations</div>
<button class="w-full text-left px-2.5 py-2 rounded-lg bg-paper text-accent font-medium">Connectors</button>
<button class="w-full text-left px-2.5 py-2 rounded-lg text-muted hover:bg-paper">Messaging routing</button>
<button class="w-full text-left px-2.5 py-2 rounded-lg text-muted hover:bg-paper">Activity</button>
<button class="w-full text-left px-2.5 py-2 rounded-lg text-muted hover:bg-paper">MCP servers</button>
</nav>
<div class="flex-1 min-w-0 overflow-y-auto">
<div class="max-w-3xl mx-auto px-8 py-8">
<!-- ============ A · CONNECTORS ============ -->
<section id="v-list">
<div class="flex items-center justify-between mb-1">
<h2 class="text-[22px] font-semibold tracking-tight">Connectors</h2>
<input placeholder="Search" class="w-44 px-3.5 py-1.5 rounded-full bg-panel text-[13px] outline-none shadow-[0_0_0_0.5px_var(--line-strong)] focus:shadow-[0_0_0_1.5px_var(--accent)]" />
</div>
<div class="grp-h">Connected</div>
<div class="grp">
<button data-goto="slack" class="row w-full text-left hover:bg-paper/50" data-brand="slack">
<span class="badge">#</span>
<span class="min-w-0 flex-1">
<span class="font-medium">Slack</span>
<span class="block text-[12px] text-muted">2 workspaces · relay</span>
</span>
<span class="dot" style="background:var(--ok-dot)"></span>
<span class="chev"></span>
</button>
<button data-goto="gmail" class="row w-full text-left hover:bg-paper/50" data-brand="gmail">
<span class="badge"></span>
<span class="min-w-0 flex-1">
<span class="font-medium">Gmail</span>
<span class="block text-[12px] text-muted">2 accounts</span>
</span>
<span class="tag tag-warn">Reauthorize</span>
<span class="chev"></span>
</button>
<button data-goto="hubspot" class="row w-full text-left hover:bg-paper/50" data-brand="hubspot">
<span class="badge">H</span>
<span class="min-w-0 flex-1">
<span class="font-medium">HubSpot</span>
<span class="block text-[12px] text-muted">2 accounts · writes ask first</span>
</span>
<span class="dot" style="background:var(--ok-dot)"></span>
<span class="chev"></span>
</button>
<button class="row w-full text-left hover:bg-paper/50" data-brand="browser">
<span class="badge"></span>
<span class="min-w-0 flex-1">
<span class="font-medium">Browser</span>
<span class="block text-[12px] text-muted">Built in</span>
</span>
<span class="dot" style="background:var(--ok-dot)"></span>
<span class="chev"></span>
</button>
</div>
<div class="grp-h">Available</div>
<div class="grp">
<div class="row" data-brand="teams">
<span class="badge">T</span>
<span class="min-w-0 flex-1 font-medium">Microsoft Teams</span>
<button data-goto="teams" class="pill pill-quiet">Connect</button>
</div>
<div class="row" data-brand="telegram">
<span class="badge"></span>
<span class="min-w-0 flex-1 font-medium">Telegram</span>
<button class="pill pill-quiet">Connect</button>
</div>
<div class="row" data-brand="github">
<span class="badge"></span>
<span class="min-w-0 flex-1 font-medium">GitHub</span>
<button class="pill pill-quiet">Connect</button>
</div>
<div class="row" data-brand="linear">
<span class="badge">L</span>
<span class="min-w-0 flex-1 font-medium">Linear</span>
<button class="pill pill-quiet">Connect</button>
</div>
</div>
<div class="foot">18 more · <button class="text-muted hover:text-ink">show all</button></div>
<div class="grp mt-6">
<div class="row">
<span class="dot" style="background:var(--ok-dot)"></span>
<span class="min-w-0 flex-1 text-[12.5px] text-muted">Signed in as <span class="text-ink font-medium">rohit@opencoworker.app</span></span>
<button class="text-[12.5px] text-muted hover:text-ink">Sign out</button>
</div>
</div>
</section>
<!-- ============ B · SLACK ============ -->
<section id="v-slack" class="hidden" data-brand="slack">
<button data-goto="list" class="text-[13px] text-accent mb-3"> Connectors</button>
<div class="flex items-center gap-3.5">
<span class="badge" style="width:44px;height:44px;font-size:19px;border-radius:10px">#</span>
<div class="min-w-0 flex-1">
<h2 class="text-[22px] font-semibold tracking-tight leading-tight">Slack</h2>
<div class="text-[12.5px] text-muted flex items-center gap-1.5">
<span class="dot" style="background:var(--ok-dot)"></span> Live · relay · sign-in · tokens
</div>
</div>
<button data-modal="slack" class="pill pill-accent"> Add workspace</button>
</div>
<div class="grp-h">deeplearning.ai <span class="font-normal text-faint">· T05F1LGKR2X</span></div>
<div class="grp">
<div class="row">
<span class="text-[12.5px] text-muted w-24 shrink-0">People</span>
<span class="flex flex-wrap gap-1.5 flex-1">
<span class="inline-flex items-center gap-1.5 pl-1 pr-2 py-0.5 rounded-full bg-paper text-[12.5px]">
<span class="w-5 h-5 rounded-full bg-accentSoft text-accent grid place-items-center text-[9px] font-bold">RP</span>
Rohit Prasad <button class="x">×</button>
</span>
<span class="inline-flex items-center gap-1.5 pl-1 pr-2 py-0.5 rounded-full bg-paper text-[12.5px]">
<span class="w-5 h-5 rounded-full bg-accentSoft text-accent grid place-items-center text-[9px] font-bold">AN</span>
Andrew Ng <button class="x">×</button>
</span>
</span>
</div>
<div class="row bg-warnSoft/25">
<span class="text-[12.5px] text-muted w-24 shrink-0">Waiting</span>
<span class="min-w-0 flex-1">
<span class="font-medium">Maya Lin</span> <span class="text-muted">in #ocw-test · 2m</span>
<span class="block text-[12.5px] text-muted truncate">“hey ocw, can you summarize this thread?”</span>
</span>
<button class="pill pill-accent !py-1">Allow & deliver</button>
<button class="pill pill-line !py-1">Allow</button>
<button class="x px-1">×</button>
</div>
<div class="row">
<span class="text-[12.5px] text-muted w-24 shrink-0">Listening</span>
<span class="min-w-0 flex-1 space-y-1">
<span class="flex items-center gap-2"><span class="font-medium">Ops · incident watch</span><span class="text-faint"></span><span class="text-muted">#incidents</span><button class="x ml-auto">×</button></span>
<span class="flex items-center gap-2"><span class="font-medium">hi</span><span class="text-faint"></span><span class="text-muted">#ocw-test · DMs</span><button class="x ml-auto">×</button></span>
</span>
</div>
<div class="row">
<span class="flex-1"></span>
<button class="text-[12.5px] text-danger/90 hover:text-danger" title="Stops relaying to this computer. @ocw stays in Slack.">Disconnect workspace</button>
</div>
</div>
<div class="grp-h">acme-partners <span class="font-normal text-faint">· T09ZZ88XX77</span></div>
<div class="grp">
<div class="row">
<span class="min-w-0 flex-1 text-[12.5px] text-muted">No one allowed yet — mentions of @ocw show up here for your OK.</span>
<button class="text-[12.5px] text-danger/90 hover:text-danger">Disconnect workspace</button>
</div>
</div>
<div class="grp mt-6">
<details>
<summary class="row hover:bg-paper/50">
<span class="text-[12.5px] text-muted w-24 shrink-0 flex items-center gap-1.5"><span class="dchev"></span> Tools</span>
<span class="min-w-0 flex-1 text-[12.5px] text-muted">2 of 2 enabled</span>
</summary>
<label class="row">
<input type="checkbox" checked />
<span class="min-w-0 flex-1"><span class="font-medium">Send messages</span></span>
<span class="tag tag-warn">asks first</span>
</label>
<label class="row">
<input type="checkbox" checked />
<span class="min-w-0 flex-1"><span class="font-medium">Read channel history</span></span>
<span class="tag tag-quiet">read</span>
</label>
</details>
</div>
<div class="foot">Names come from Slack automatically. IDs show on hover.</div>
</section>
<!-- ============ C · GMAIL ============ -->
<section id="v-gmail" class="hidden" data-brand="gmail">
<button data-goto="list" class="text-[13px] text-accent mb-3"> Connectors</button>
<div class="flex items-center gap-3.5">
<span class="badge" style="width:44px;height:44px;font-size:19px;border-radius:10px"></span>
<div class="min-w-0 flex-1">
<h2 class="text-[22px] font-semibold tracking-tight leading-tight">Gmail</h2>
<div class="text-[12.5px] text-muted flex items-center gap-1.5">
<span class="dot bg-warnInk"></span> team@dl.ai needs reauthorizing
</div>
</div>
<button class="pill pill-accent"> Add account</button>
</div>
<div class="grp-h">Accounts</div>
<div class="grp">
<div class="row">
<span class="badge-sm"></span>
<span class="min-w-0 flex-1">
<span class="font-medium">rohit@dl.ai</span> <span class="tag tag-accent ml-1">Default</span>
<span class="block text-[12px] text-muted">Read · draft · send</span>
</span>
<span class="dot" style="background:var(--ok-dot)"></span>
<button class="x px-1" title="Disconnect">×</button>
</div>
<div class="row">
<span class="badge-sm"></span>
<span class="min-w-0 flex-1">
<span class="font-medium">team@dl.ai</span>
<span class="block text-[12px] text-warnInk">Google revoked access — sign in again</span>
</span>
<button class="pill pill-accent !py-1">Reauthorize</button>
<button class="x px-1" title="Disconnect">×</button>
</div>
</div>
<div class="foot">Agents use the Default account unless a session picks another. Every tool call shows the account it ran as.</div>
<div class="grp-h">Never show agents</div>
<div class="grp">
<div class="row">
<span class="text-[12.5px] text-muted w-24 shrink-0">Senders</span>
<span class="min-w-0 flex-1 flex flex-wrap items-center gap-1.5">
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-paper text-[12px] font-mono">finance@dl.ai <button class="x">×</button></span>
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-paper text-[12px] font-mono">@payroll.dl.ai <button class="x">×</button></span>
<button class="text-[12px] text-accent">+ Add</button>
</span>
</div>
<div class="row">
<span class="text-[12.5px] text-muted w-24 shrink-0">Labels</span>
<span class="min-w-0 flex-1 flex flex-wrap items-center gap-1.5">
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-paper text-[12px]">Confidential <button class="x">×</button></span>
<button class="text-[12px] text-accent">+ Add</button>
</span>
</div>
</div>
<div class="foot">These emails don't exist as far as agents can tell — no sender, no subject, no “redacted” marker. When a search skips some, you see the count on the tool card; agents don't.</div>
<div class="grp mt-6">
<details>
<summary class="row hover:bg-paper/50">
<span class="text-[12.5px] text-muted w-24 shrink-0 flex items-center gap-1.5"><span class="dchev"></span> Tools</span>
<span class="min-w-0 flex-1 text-[12.5px] text-muted">3 of 4 enabled</span>
</summary>
<label class="row">
<input type="checkbox" checked />
<span class="min-w-0 flex-1"><span class="font-medium">Search & read mail</span></span>
<span class="tag tag-quiet">read</span>
</label>
<label class="row">
<input type="checkbox" checked />
<span class="min-w-0 flex-1"><span class="font-medium">Summarize threads</span></span>
<span class="tag tag-quiet">read</span>
</label>
<label class="row">
<input type="checkbox" checked />
<span class="min-w-0 flex-1"><span class="font-medium">Create drafts</span></span>
<span class="tag tag-warn">asks first</span>
</label>
<label class="row">
<input type="checkbox" />
<span class="min-w-0 flex-1"><span class="font-medium">Send email</span></span>
<span class="tag tag-warn">asks first</span>
</label>
</details>
</div>
</section>
<!-- ============ D · HUBSPOT ============ -->
<section id="v-hubspot" class="hidden" data-brand="hubspot">
<button data-goto="list" class="text-[13px] text-accent mb-3"> Connectors</button>
<div class="flex items-center gap-3.5">
<span class="badge" style="width:44px;height:44px;font-size:19px;border-radius:10px">H</span>
<div class="min-w-0 flex-1">
<h2 class="text-[22px] font-semibold tracking-tight leading-tight">HubSpot</h2>
<div class="text-[12.5px] text-muted flex items-center gap-1.5">
<span class="dot" style="background:var(--ok-dot)"></span> Live · 2 accounts
</div>
</div>
<button data-modal="hubspot" class="pill pill-accent"> Add account</button>
</div>
<div class="grp-h">Accounts</div>
<div class="grp">
<div class="row">
<span class="badge-sm">H</span>
<span class="min-w-0 flex-1">
<span class="font-medium">DeepLearning.AI</span> <span class="tag tag-accent ml-1">Default</span>
<span class="block text-[12px] text-muted">Sales Hub Pro · used 20m ago</span>
</span>
<span class="dot" style="background:var(--ok-dot)"></span>
<button class="x px-1" title="Disconnect">×</button>
</div>
<div class="row">
<span class="badge-sm">H</span>
<span class="min-w-0 flex-1">
<span class="font-medium">DLAI Sandbox</span> <span class="tag tag-warn ml-1">Sandbox</span>
<span class="block text-[12px] text-muted">Writes skip approval here</span>
</span>
<span class="dot" style="background:var(--ok-dot)"></span>
<button class="x px-1" title="Disconnect">×</button>
</div>
</div>
<div class="grp-h">Access & privacy</div>
<div class="grp">
<div class="row">
<span class="text-[12.5px] text-muted w-28 shrink-0">Access</span>
<span class="min-w-0 flex-1">
<span class="font-medium">Read & write</span> <span class="text-muted">· as rohit@dl.ai</span>
<span class="block text-[12px] text-muted">Agents see only what this HubSpot user can see — narrow it with HubSpot permission sets.</span>
</span>
</div>
<div class="row">
<span class="text-[12.5px] text-muted w-28 shrink-0">Hidden fields</span>
<span class="min-w-0 flex-1 flex flex-wrap items-center gap-1.5">
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-paper text-[12px] font-mono">salary <button class="x">×</button></span>
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-paper text-[12px] font-mono">ssn <button class="x">×</button></span>
<button class="text-[12px] text-accent">+ Add</button>
</span>
<span class="text-[12px] text-faint">never shown to models</span>
</div>
<div class="row">
<span class="text-[12.5px] text-muted w-28 shrink-0">Audit</span>
<span class="min-w-0 flex-1 text-[12.5px]">Every call is logged with session, tool and record.</span>
<button class="text-[12.5px] text-accent">View activity </button>
</div>
<details>
<summary class="row hover:bg-paper/50">
<span class="text-[12.5px] text-muted w-28 shrink-0 flex items-center gap-1.5"><span class="dchev"></span> Tools</span>
<span class="min-w-0 flex-1 text-[12.5px] text-muted">3 of 5 enabled</span>
</summary>
<label class="row">
<input type="checkbox" checked />
<span class="min-w-0 flex-1"><span class="font-medium">Search & read CRM</span></span>
<span class="tag tag-quiet">read</span>
</label>
<label class="row">
<input type="checkbox" checked />
<span class="min-w-0 flex-1"><span class="font-medium">Log notes & tasks</span></span>
<span class="tag tag-warn">asks first</span>
</label>
<label class="row">
<input type="checkbox" checked />
<span class="min-w-0 flex-1"><span class="font-medium">Create & update records</span></span>
<span class="tag tag-warn">asks first</span>
</label>
<label class="row">
<input type="checkbox" />
<span class="min-w-0 flex-1"><span class="font-medium">Send email via HubSpot</span></span>
<span class="tag tag-warn">asks first</span>
</label>
<div class="row opacity-50">
<input type="checkbox" disabled />
<span class="min-w-0 flex-1"><span class="font-medium">Delete records</span></span>
<span class="tag tag-quiet">never</span>
</div>
</details>
</div>
<div class="foot">Approvals name the account they'll write to — a sandbox test can't quietly hit production.</div>
</section>
<!-- ============ E · TEAMS (corrected consent model) ============ -->
<section id="v-teams" class="hidden" data-brand="teams">
<button data-goto="list" class="text-[13px] text-accent mb-3"> Connectors</button>
<div class="flex items-center gap-3.5">
<span class="badge" style="width:44px;height:44px;font-size:19px;border-radius:10px">T</span>
<div class="min-w-0 flex-1">
<h2 class="text-[22px] font-semibold tracking-tight leading-tight">Microsoft Teams <span class="tag tag-warn align-middle">Concept</span></h2>
<div class="text-[12.5px] text-muted flex items-center gap-1.5">
<span class="dot" style="background:var(--ok-dot)"></span> Live · 1 organization
</div>
</div>
<button class="pill pill-accent"> Add to Teams</button>
</div>
<div class="grp-h">DeepLearning.AI <span class="font-normal text-faint">· tenant 72f9…4a1c</span></div>
<div class="grp">
<div class="row">
<span class="text-[12.5px] text-muted w-24 shrink-0">People</span>
<span class="flex-1">
<span class="inline-flex items-center gap-1.5 pl-1 pr-2 py-0.5 rounded-full bg-paper text-[12.5px]">
<span class="w-5 h-5 rounded-full bg-accentSoft text-accent grid place-items-center text-[9px] font-bold">RP</span>
Rohit Prasad <button class="x">×</button>
</span>
</span>
</div>
<div class="row">
<span class="text-[12.5px] text-muted w-24 shrink-0">Listening</span>
<span class="min-w-0 flex-1 flex items-center gap-2"><span class="font-medium">hi</span><span class="text-faint"></span><span class="text-muted">Chat with @ocw</span><button class="x ml-auto">×</button></span>
</div>
<div class="row">
<span class="flex-1"></span>
<button class="text-[12.5px] text-danger/90 hover:text-danger">Disconnect organization</button>
</div>
</div>
<div class="grp-h">How connecting works</div>
<div class="grp">
<div class="row">
<span class="text-[12.5px] text-muted w-24 shrink-0">Chats</span>
<span class="flex-1 text-[12.5px]">Add @ocw yourself — no admin needed.</span>
</div>
<div class="row">
<span class="text-[12.5px] text-muted w-24 shrink-0">Channels</span>
<span class="flex-1 text-[12.5px]">Any <span class="font-medium">team owner</span> adds it to their team and consents for that team only.</span>
</div>
<div class="row">
<span class="text-[12.5px] text-muted w-24 shrink-0">Blocked?</span>
<span class="flex-1 text-[12.5px]">If your org blocks new apps, Teams shows <span class="font-medium">Request approval</span> — it asks your IT admin for you, once for the whole org.</span>
</div>
</div>
<div class="foot">Relay-only: Teams bots need a public endpoint, so there's no manual mode.</div>
</section>
</div>
</div>
</div>
<!-- ============ Add-connection modals (one entry point: the header button) ============ -->
<div id="modal-backdrop" class="hidden fixed inset-0 z-40" style="background:rgba(0,0,0,.32)"></div>
<div id="modal-slack" data-brand="slack" class="hidden fixed z-50 left-1/2 top-[16%] -translate-x-1/2 w-[480px] bg-panel rounded-2xl shadow-2xl" style="box-shadow:0 0 0 0.5px var(--line-strong), 0 24px 60px rgba(0,0,0,.25)">
<div class="flex items-center gap-3 px-5 pt-5">
<span class="badge">#</span>
<div class="flex-1 font-semibold text-[16px] tracking-tight">Add a workspace</div>
<button class="modal-close x text-[18px]">×</button>
</div>
<div class="px-5 pt-4">
<div class="inline-flex rounded-full p-0.5 bg-paper text-[12.5px] font-medium">
<button data-pane="one" class="seg px-3.5 py-1 rounded-full">One click</button>
<button data-pane="manual" class="seg px-3.5 py-1 rounded-full">Manual</button>
</div>
</div>
<div data-pane-body="one" class="px-5 py-4 space-y-3">
<p class="text-[13px] text-muted">Opens Slack in your browser — approve @ocw for the workspace. No tokens, works for any number of workspaces.</p>
<button class="pill pill-accent w-full !py-2 text-center">Add to Slack</button>
<p class="text-[12px] text-faint text-center">Recommended · messages relay through OpenCoworker Cloud, tokens stay on this computer</p>
</div>
<div data-pane-body="manual" class="hidden px-5 py-4 space-y-3">
<ol class="list-decimal pl-4 text-[13px] text-muted space-y-1">
<li>Create an app at api.slack.com/apps</li>
<li>Enable <span class="font-mono text-[12px]">Socket Mode</span>, add bot scopes, install it</li>
<li>Paste both tokens</li>
</ol>
<input type="password" placeholder="Bot token · xoxb-…" class="w-full px-3 py-2 rounded-lg bg-paper text-[13px] outline-none shadow-[0_0_0_0.5px_var(--line-strong)] focus:shadow-[0_0_0_1.5px_var(--accent)]" />
<input type="password" placeholder="App token · xapp-…" class="w-full px-3 py-2 rounded-lg bg-paper text-[13px] outline-none shadow-[0_0_0_0.5px_var(--line-strong)] focus:shadow-[0_0_0_1.5px_var(--accent)]" />
<button class="pill pill-accent w-full !py-2 text-center">Connect</button>
<p class="text-[12px] text-warnInk text-center">One mode at a time — this pauses your relay workspaces.</p>
</div>
</div>
<div id="modal-hubspot" data-brand="hubspot" class="hidden fixed z-50 left-1/2 top-[16%] -translate-x-1/2 w-[480px] bg-panel rounded-2xl" style="box-shadow:0 0 0 0.5px var(--line-strong), 0 24px 60px rgba(0,0,0,.25)">
<div class="flex items-center gap-3 px-5 pt-5">
<span class="badge">H</span>
<div class="flex-1 font-semibold text-[16px] tracking-tight">Add a HubSpot account</div>
<button class="modal-close x text-[18px]">×</button>
</div>
<div class="px-5 pt-4">
<div class="inline-flex rounded-full p-0.5 bg-paper text-[12.5px] font-medium">
<button data-pane="one" class="seg px-3.5 py-1 rounded-full">One click</button>
<button data-pane="manual" class="seg px-3.5 py-1 rounded-full">Manual</button>
</div>
</div>
<div data-pane-body="one" class="px-5 py-4 space-y-3">
<p class="text-[13px] text-muted">Opens HubSpot in your browser — pick which account (portal) to grant. Repeat for each portal.</p>
<div class="flex items-center justify-center gap-5 text-[13px]">
<label class="flex items-center gap-1.5"><input type="radio" name="hs-access" checked /> Read-only</label>
<label class="flex items-center gap-1.5"><input type="radio" name="hs-access" /> Read & write</label>
</div>
<button class="pill pill-accent w-full !py-2 text-center">Connect HubSpot</button>
<p class="text-[12px] text-faint text-center">Recommended · read-only can never modify your CRM</p>
</div>
<div data-pane-body="manual" class="hidden px-5 py-4 space-y-3">
<ol class="list-decimal pl-4 text-[13px] text-muted space-y-1">
<li>HubSpot → Settings → Integrations → <span class="font-mono text-[12px]">Private Apps</span></li>
<li>Create one with the scopes agents should have</li>
<li>Paste the token</li>
</ol>
<input type="password" placeholder="pat-na1-…" class="w-full px-3 py-2 rounded-lg bg-paper text-[13px] outline-none shadow-[0_0_0_0.5px_var(--line-strong)] focus:shadow-[0_0_0_1.5px_var(--accent)]" />
<button class="pill pill-accent w-full !py-2 text-center">Connect</button>
<p class="text-[12px] text-faint text-center">Works without cloud sign-in</p>
</div>
</div>
<script>
const views = { list:'v-list', slack:'v-slack', gmail:'v-gmail', hubspot:'v-hubspot', teams:'v-teams' };
const tabs = document.querySelectorAll('.view-tab');
function show(name){
Object.entries(views).forEach(([k,id]) => document.getElementById(id).classList.toggle('hidden', k!==name));
tabs.forEach(t => {
const on = t.dataset.view===name;
t.classList.toggle('bg-panel',on); t.classList.toggle('shadow-sm',on);
t.classList.toggle('text-ink',on); t.classList.toggle('text-muted',!on);
});
}
tabs.forEach(t => t.addEventListener('click', () => show(t.dataset.view)));
document.querySelectorAll('[data-goto]').forEach(b => b.addEventListener('click', e => { e.stopPropagation(); show(b.dataset.goto); }));
document.getElementById('themeBtn').addEventListener('click', () => {
document.documentElement.dataset.theme = document.documentElement.dataset.theme==='dark' ? 'light' : 'dark';
});
// add-connection modals: header button → sheet with a One click | Manual switcher
const backdrop = document.getElementById('modal-backdrop');
function closeModals(){
backdrop.classList.add('hidden');
document.querySelectorAll('[id^="modal-"]').forEach(m => m.id!=='modal-backdrop' && m.classList.add('hidden'));
}
function openModal(name){
closeModals();
backdrop.classList.remove('hidden');
const m = document.getElementById('modal-'+name);
m.classList.remove('hidden');
setPane(m, 'one');
}
function setPane(modal, pane){
modal.querySelectorAll('[data-pane-body]').forEach(p => p.classList.toggle('hidden', p.dataset.paneBody!==pane));
modal.querySelectorAll('.seg').forEach(s => {
const on = s.dataset.pane===pane;
s.classList.toggle('bg-panel',on); s.classList.toggle('shadow-sm',on);
s.classList.toggle('text-ink',on); s.classList.toggle('text-muted',!on);
});
}
document.querySelectorAll('[data-modal]').forEach(b => b.addEventListener('click', () => openModal(b.dataset.modal)));
document.querySelectorAll('.seg').forEach(s => s.addEventListener('click', () => setPane(s.closest('[id^="modal-"]'), s.dataset.pane)));
document.querySelectorAll('.modal-close').forEach(b => b.addEventListener('click', closeModals));
backdrop.addEventListener('click', closeModals);
document.addEventListener('keydown', e => e.key==='Escape' && closeModals());
show('list');
</script>
</body>
</html>

919
ui-mocks/redesign.html Normal file
View File

@@ -0,0 +1,919 @@
<!doctype html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenCoworker — UI redesign mockups</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: ['selector', '[data-theme="dark"]'],
theme: {
extend: {
colors: {
paper: 'var(--paper)', panel: 'var(--panel)', ink: 'var(--ink)', muted: 'var(--muted)',
faint: 'var(--faint)', line: 'var(--line)', lineStrong: 'var(--line-strong)',
accent: 'var(--accent)', accentSoft: 'var(--accent-soft)', ok: 'var(--ok)',
okSoft: 'var(--ok-soft)', okLine: 'var(--ok-line)', warnInk: 'var(--warn-ink)',
warnSoft: 'var(--warn-soft)', danger: 'var(--danger)', tealInk: 'var(--teal-ink)',
tealSoft: 'var(--teal-soft)', tealLine: 'var(--teal-line)', solid: 'var(--solid)',
onSolid: 'var(--on-solid)',
},
fontFamily: {
sans: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Inter', 'system-ui', 'sans-serif'],
mono: ['SF Mono', 'JetBrains Mono', 'Menlo', 'monospace'],
},
borderRadius: { xl2: '14px' },
},
},
};
</script>
<style>
:root{
--paper:#f5f6f7; --panel:#ffffff; --ink:#17191c; --muted:#5b616b; --faint:#9aa1aa;
--line:#e8eaed; --line-strong:#d8dce1; --accent:#2563eb; --accent-soft:#e9f0fd;
--ok:#2f7d57; --ok-soft:#eef6f0; --ok-line:#cfe6d6; --warn-ink:#b45309; --warn-soft:#fef3c7;
--danger:#b91c1c; --teal-ink:#0f766e; --teal-line:#cfebea; --teal-soft:#edfafa;
--solid:#17191c; --on-solid:#fff;
}
html[data-theme="dark"]{
--paper:#131417; --panel:#1c1e22; --ink:#e6e8eb; --muted:#9aa1ab; --faint:#62686f;
--line:#2a2d33; --line-strong:#3a3e46; --accent:#4c8dff; --accent-soft:#1c2a44;
--ok:#58b07f; --ok-soft:#1a2b21; --ok-line:#2c4736; --warn-ink:#e8b04b; --warn-soft:#36280f;
--danger:#f07067; --teal-ink:#46c0b2; --teal-line:#1f4a45; --teal-soft:#11302d;
--solid:#2e3138; --on-solid:#e6e8eb;
}
body{ background:var(--paper); color:var(--ink); }
/* brand tints for connector cards/badges */
[data-brand="slack"] { --brand:#611f69; --brand-soft:#f4ecf5; }
[data-brand="salesforce"] { --brand:#00a1e0; --brand-soft:#e6f6fd; }
[data-brand="hubspot"] { --brand:#ff7a59; --brand-soft:#fff0ec; }
[data-brand="telegram"] { --brand:#229ed9; --brand-soft:#e6f4fb; }
[data-brand="github"] { --brand:#1f2328; --brand-soft:#eef0f2; }
[data-brand="datadog"] { --brand:#632ca6; --brand-soft:#f0eafa; }
[data-brand="mcp"] { --brand:#6b7280; --brand-soft:#eef0f2; }
[data-brand="default"] { --brand:#6b7280; --brand-soft:#eef0f2; } /* fallback connector */
html[data-theme="dark"] [data-brand="slack"] { --brand-soft:#2a1430; }
html[data-theme="dark"] [data-brand="salesforce"] { --brand-soft:#0a2433; }
html[data-theme="dark"] [data-brand="hubspot"] { --brand-soft:#331a12; }
html[data-theme="dark"] [data-brand="telegram"] { --brand-soft:#0c2533; }
html[data-theme="dark"] [data-brand="github"] { --brand:#e6e8eb; --brand-soft:#26282d; }
html[data-theme="dark"] [data-brand="datadog"] { --brand-soft:#241634; }
html[data-theme="dark"] [data-brand="mcp"] { --brand-soft:#26282d; }
html[data-theme="dark"] [data-brand="default"] { --brand-soft:#26282d; }
/* native <details> disclosure for collapsed steps */
details > summary{ list-style:none; }
details > summary::-webkit-details-marker{ display:none; }
details[open] .chev{ transform:rotate(90deg); }
details[open] .when-closed{ display:none; }
.src-ava{ width:24px; height:24px; padding:4px; border-radius:9999px; background:var(--panel); border:1px solid var(--line); box-shadow:0 0 0 2px var(--paper); display:grid; place-items:center; }
.src-badge{ width:32px; height:32px; padding:6px; border-radius:9px; background:var(--brand-soft); display:grid; place-items:center; flex:none; }
.tgl{ width:34px; height:20px; border-radius:9999px; background:var(--line-strong); position:relative; flex:none; cursor:pointer; transition:background .15s; }
.tgl[aria-checked="true"]{ background:var(--accent); }
.tgl .knob{ position:absolute; top:2px; left:2px; width:16px; height:16px; border-radius:9999px; background:#fff; transition:left .15s; box-shadow:0 1px 2px rgba(0,0,0,.2); }
.tgl[aria-checked="true"] .knob{ left:16px; }
.hairline-scroll::-webkit-scrollbar{ width:9px; height:9px; }
.hairline-scroll::-webkit-scrollbar-thumb{ background:var(--line-strong); border-radius:9px; border:2px solid var(--paper); }
.id-reveal .id-hidden{ display:none; }
.id-reveal:hover .id-hidden{ display:inline; }
.id-reveal:hover .id-name{ display:none; }
</style>
</head>
<body class="font-sans antialiased text-[14px]">
<!-- ============ review-only switcher bar (not part of the product) ============ -->
<div class="flex items-center gap-3 px-4 h-11 bg-panel border-b border-line text-[12.5px]">
<span class="font-semibold tracking-tight">OpenCoworker · redesign</span>
<span class="text-faint">static mock</span>
<div class="ml-2 inline-flex rounded-lg border border-line p-0.5 bg-paper">
<button data-view="session" class="view-tab px-3 py-1 rounded-md font-medium">Agent session</button>
<button data-view="integrations" class="view-tab px-3 py-1 rounded-md font-medium">Integrations</button>
</div>
<div class="ml-auto flex items-center gap-2">
<button id="themeBtn" class="px-2.5 py-1 rounded-md border border-line bg-paper hover:border-lineStrong">◐ Theme</button>
</div>
</div>
<!-- ============ app frame ============ -->
<div class="flex" style="height:calc(100vh - 44px)">
<!-- ---------- sidebar (shared chrome) ---------- -->
<aside class="w-[264px] shrink-0 bg-panel border-r border-line flex flex-col">
<div class="px-3.5 pt-3.5 pb-2 flex items-center gap-2">
<div class="w-6 h-6 rounded-md bg-accent/15 grid place-items-center text-accent">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M5 13l4 4L19 7"/></svg>
</div>
<div class="font-semibold tracking-tight">OpenCoworker</div>
<button id="layoutToggle" class="ml-auto w-7 h-7 grid place-items-center rounded-md text-faint hover:text-ink hover:bg-paper" title="Layout: flat ↔ group by persona">
<svg id="layoutIcon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 5h18M3 12h18M3 19h18"/></svg>
</button>
</div>
<!-- New session: split button — primary uses last/default persona; ▾ picks a persona (§8) -->
<div class="px-3 pb-2 relative">
<div class="flex">
<button class="flex-1 text-left px-3 py-2 rounded-l-lg bg-accent text-white text-[13px] font-medium hover:opacity-95 flex items-center gap-2">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
New session
</button>
<button id="personaMenuBtn" class="px-2.5 rounded-r-lg bg-accent text-white border-l border-white/25 hover:opacity-95" title="Start with a specific persona">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M6 9l6 6 6-6"/></svg>
</button>
</div>
<div id="personaMenu" class="hidden absolute left-3 right-3 mt-1 z-30 bg-panel border border-line rounded-xl2 shadow-xl p-1">
<div class="px-2 py-1 text-[10.5px] uppercase tracking-[0.06em] text-faint font-semibold">Start a session as</div>
<button class="w-full flex items-center gap-2.5 px-2 py-1.5 rounded-lg hover:bg-paper text-left"><span class="w-6 h-6 rounded-md bg-paper border border-line grid place-items-center text-[13px]">🛠️</span><span><span class="block text-[13px] font-medium">Ops</span><span class="block text-[11px] text-muted">incidents, runbooks, infra</span></span></button>
<button class="w-full flex items-center gap-2.5 px-2 py-1.5 rounded-lg hover:bg-paper text-left"><span class="w-6 h-6 rounded-md bg-paper border border-line grid place-items-center text-[13px]">💬</span><span><span class="block text-[13px] font-medium">Cowork</span><span class="block text-[11px] text-muted">general assistant</span></span></button>
<button class="w-full flex items-center gap-2.5 px-2 py-1.5 rounded-lg hover:bg-paper text-left"><span class="w-6 h-6 rounded-md bg-paper border border-line grid place-items-center text-[13px]">🧰</span><span><span class="block text-[13px] font-medium">Code</span><span class="block text-[11px] text-muted">repository work</span></span></button>
<button class="w-full flex items-center gap-2.5 px-2 py-1.5 rounded-lg hover:bg-paper text-left"><span class="w-6 h-6 rounded-md bg-paper border border-line grid place-items-center text-[13px]">📊</span><span><span class="block text-[13px] font-medium">Analyst</span><span class="block text-[11px] text-muted">data & reporting</span></span></button>
<div class="border-t border-line mt-1 pt-1"><button id="managePersonas" class="w-full px-2 py-1.5 rounded-lg hover:bg-paper text-left text-[12.5px] text-muted">Manage personas…</button></div>
</div>
</div>
<div class="px-3 mt-1 flex items-center gap-2">
<div class="flex-1 flex items-center gap-2 px-2.5 py-1.5 rounded-lg bg-paper border border-line text-faint">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4-4"/></svg>
<span class="text-[12.5px]">Search</span>
</div>
</div>
<div class="flex-1 overflow-y-auto hairline-scroll px-2.5 mt-3">
<div id="list-flat" class="space-y-4">
<div>
<div class="px-1.5 text-[10.5px] uppercase tracking-[0.07em] text-faint font-semibold mb-1">Pinned</div>
<!-- session row: subscribed + working -->
<button class="w-full group flex items-center gap-2.5 px-2 py-2 rounded-lg bg-accentSoft/60 border border-accent/30 text-left">
<span class="w-6 h-6 rounded-md bg-panel border border-line grid place-items-center text-[13px]">🛠️</span>
<span class="min-w-0 flex-1">
<span class="block truncate text-[13px] font-medium">Ops · incident watch</span>
<span class="block truncate text-[11px] text-muted">SRE persona · #incidents</span>
</span>
<span class="flex items-center gap-1.5 shrink-0">
<span title="Slack" class="text-faint" data-brand="slack">&#x25CF;</span>
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" title="working"></span>
</span>
</button>
</div>
<div>
<div class="px-1.5 text-[10.5px] uppercase tracking-[0.07em] text-faint font-semibold mb-1">Recent</div>
<div class="space-y-0.5">
<button class="w-full flex items-center gap-2.5 px-2 py-2 rounded-lg hover:bg-paper text-left">
<span class="w-6 h-6 rounded-md bg-panel border border-line grid place-items-center text-[13px]">💬</span>
<span class="min-w-0 flex-1">
<span class="block truncate text-[13px] font-medium">hi</span>
<span class="block truncate text-[11px] text-muted">Cowork · 2 channels</span>
</span>
<span class="flex items-center gap-1.5 shrink-0">
<span title="Slack" data-brand="slack" style="color:var(--brand)">&#x25CF;</span>
<span class="text-[10px] font-semibold text-white bg-accent rounded-full px-1.5 leading-[15px]" title="1 needs attention">1</span>
</span>
</button>
<button class="w-full flex items-center gap-2.5 px-2 py-2 rounded-lg hover:bg-paper text-left">
<span class="w-6 h-6 rounded-md bg-panel border border-line grid place-items-center text-[13px]">📊</span>
<span class="min-w-0 flex-1">
<span class="block truncate text-[13px] font-medium">Q1 sales analysis</span>
<span class="block truncate text-[11px] text-muted">Cowork</span>
</span>
<span class="w-1.5 h-1.5 rounded-full bg-faint/60" title="idle"></span>
</button>
<button class="w-full flex items-center gap-2.5 px-2 py-2 rounded-lg hover:bg-paper text-left">
<span class="w-6 h-6 rounded-md bg-panel border border-line grid place-items-center text-[13px]">🧰</span>
<span class="min-w-0 flex-1">
<span class="block truncate text-[13px] font-medium">Refactor auth module</span>
<span class="block truncate text-[11px] text-muted">Code · ~/app</span>
</span>
<span class="w-1.5 h-1.5 rounded-full bg-faint/60" title="idle"></span>
</button>
</div>
</div>
</div><!-- /list-flat -->
<!-- grouped-by-persona layout (toggled) — each persona is a bounded card with a gear → persona page -->
<div id="list-grouped" class="hidden space-y-2.5">
<div class="rounded-xl border border-line bg-paper/50 p-1.5">
<div class="flex items-center gap-2 px-1.5 py-0.5">
<span class="text-[13px]">🛠️</span>
<span class="text-[11px] uppercase tracking-[0.06em] text-faint font-semibold">Ops</span>
<span class="text-[10px] text-faint">2</span>
<button class="persona-gear ml-auto w-6 h-6 grid place-items-center rounded-md text-faint hover:text-ink hover:bg-panel" title="About the Ops persona">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 00.3 1.9l.1.1a2 2 0 11-2.8 2.8l-.1-.1a1.7 1.7 0 00-2.9 1.2V21a2 2 0 11-4 0v-.1A1.7 1.7 0 005 19.4l-.1.1a2 2 0 11-2.8-2.8l.1-.1a1.7 1.7 0 00-1.2-2.9H1a2 2 0 110-4h.1A1.7 1.7 0 002.6 5l-.1-.1a2 2 0 112.8-2.8l.1.1A1.7 1.7 0 009 2.6V2a2 2 0 114 0v.1A1.7 1.7 0 0019 5l.1-.1a2 2 0 112.8 2.8l-.1.1a1.7 1.7 0 00-.4 1.9z" transform="scale(0.78) translate(3 3)"/></svg>
</button>
</div>
<div class="space-y-0.5 mt-0.5">
<button class="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg bg-accentSoft/70 border border-accent/30 text-left"><span class="min-w-0 flex-1 truncate text-[13px] font-medium">incident watch</span><span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" title="working"></span></button>
<button class="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-panel text-left"><span class="min-w-0 flex-1 truncate text-[13px]">prod deploy review</span></button>
</div>
</div>
<div class="rounded-xl border border-line bg-paper/50 p-1.5">
<div class="flex items-center gap-2 px-1.5 py-0.5">
<span class="text-[13px]">💬</span>
<span class="text-[11px] uppercase tracking-[0.06em] text-faint font-semibold">Cowork</span>
<span class="text-[10px] text-faint">2</span>
<button class="persona-gear ml-auto w-6 h-6 grid place-items-center rounded-md text-faint hover:text-ink hover:bg-panel" title="About the Cowork persona">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 00.3 1.9l.1.1a2 2 0 11-2.8 2.8l-.1-.1a1.7 1.7 0 00-2.9 1.2V21a2 2 0 11-4 0v-.1A1.7 1.7 0 005 19.4l-.1.1a2 2 0 11-2.8-2.8l.1-.1a1.7 1.7 0 00-1.2-2.9H1a2 2 0 110-4h.1A1.7 1.7 0 002.6 5l-.1-.1a2 2 0 112.8-2.8l.1.1A1.7 1.7 0 009 2.6V2a2 2 0 114 0v.1A1.7 1.7 0 0019 5l.1-.1a2 2 0 112.8 2.8l-.1.1a1.7 1.7 0 00-.4 1.9z" transform="scale(0.78) translate(3 3)"/></svg>
</button>
</div>
<div class="space-y-0.5 mt-0.5">
<button class="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-panel text-left"><span class="min-w-0 flex-1 truncate text-[13px]">hi</span><span class="text-[10px] font-semibold text-white bg-accent rounded-full px-1.5 leading-[15px]">1</span></button>
<button class="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-panel text-left"><span class="min-w-0 flex-1 truncate text-[13px]">Q1 sales analysis</span></button>
</div>
</div>
<div class="rounded-xl border border-line bg-paper/50 p-1.5">
<div class="flex items-center gap-2 px-1.5 py-0.5">
<span class="text-[13px]">🧰</span>
<span class="text-[11px] uppercase tracking-[0.06em] text-faint font-semibold">Code</span>
<span class="text-[10px] text-faint">1</span>
<button class="persona-gear ml-auto w-6 h-6 grid place-items-center rounded-md text-faint hover:text-ink hover:bg-panel" title="About the Code persona">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 00.3 1.9l.1.1a2 2 0 11-2.8 2.8l-.1-.1a1.7 1.7 0 00-2.9 1.2V21a2 2 0 11-4 0v-.1A1.7 1.7 0 005 19.4l-.1.1a2 2 0 11-2.8-2.8l.1-.1a1.7 1.7 0 00-1.2-2.9H1a2 2 0 110-4h.1A1.7 1.7 0 002.6 5l-.1-.1a2 2 0 112.8-2.8l.1.1A1.7 1.7 0 009 2.6V2a2 2 0 114 0v.1A1.7 1.7 0 0019 5l.1-.1a2 2 0 112.8 2.8l-.1.1a1.7 1.7 0 00-.4 1.9z" transform="scale(0.78) translate(3 3)"/></svg>
</button>
</div>
<div class="space-y-0.5 mt-0.5">
<button class="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-panel text-left"><span class="min-w-0 flex-1 truncate text-[13px]">Refactor auth module</span></button>
</div>
</div>
</div>
</div>
<!-- sidebar nav -->
<div class="px-2.5 py-2 border-t border-line space-y-0.5">
<button data-view="integrations" class="nav-int w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg hover:bg-paper text-[13px] text-left">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M9 7V4a1 1 0 011-1h4a1 1 0 011 1v3"/><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M3 12h18"/></svg>
Integrations
</button>
<button class="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg hover:bg-paper text-[13px] text-left">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
Automations
</button>
<button class="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg hover:bg-paper text-[13px] text-left">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V5M4 19h16M8 16v-5M13 16V8M18 16v-9"/></svg>
Activity
</button>
</div>
<div class="px-3.5 py-2.5 border-t border-line text-[11.5px] text-muted flex items-center justify-between">
<span class="truncate">~/fleet/ro4d</span>
<button class="text-faint hover:text-ink"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 00-.1-1l2-1.6-2-3.4-2.3 1a7 7 0 00-1.7-1L16.5 3h-4l-.4 2.4a7 7 0 00-1.7 1l-2.3-1-2 3.4 2 1.6a7 7 0 000 2l-2 1.6 2 3.4 2.3-1a7 7 0 001.7 1l.4 2.4h4l.4-2.4a7 7 0 001.7-1l2.3 1 2-3.4-2-1.6c.1-.3.1-.7.1-1z"/></svg></button>
</div>
</aside>
<!-- ============================ MAIN: SESSION VIEW ============================ -->
<main id="view-session" class="flex-1 min-w-0 flex flex-col bg-paper">
<!-- topbar -->
<div class="h-12 shrink-0 px-5 flex items-center gap-3 border-b border-line bg-panel/70 backdrop-blur">
<div class="font-semibold text-[13.5px] flex items-center gap-2 min-w-0">
<span class="truncate">Ops · incident watch</span>
<span class="text-faint font-normal">·</span>
<span class="text-muted font-normal truncate">SRE persona</span>
</div>
<!-- Model + permission-mode chips removed: they duplicated the composer's model dropdown
and "Ask for approval" control (with inconsistent wording). The composer owns both. -->
<div class="ml-auto flex items-center gap-1.5 text-[12px]"></div>
</div>
<!-- Sources: icons only; click opens the per-session connections panel (ask #2) -->
<div class="px-5 py-2 border-b border-line bg-panel/40 flex items-center">
<button id="openSources" class="group inline-flex items-center gap-2.5 -ml-2 px-2 py-1 rounded-lg hover:bg-paper" title="Manage this session's connections">
<span class="text-[11px] uppercase tracking-[0.06em] text-faint font-semibold">Sources</span>
<span class="flex items-center">
<span class="src-ava" data-brand="slack" title="Slack · #ocw-test, DMs">{{SLACK_ICON}}</span>
<span class="src-ava -ml-1.5" data-brand="github" title="GitHub · deeplearning-ai/platform">{{GH_ICON}}</span>
</span>
<span class="inline-flex items-center gap-1.5 text-[11px] text-faint group-hover:text-muted">
<span class="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded-full bg-warnSoft/70 text-warnInk border border-warnInk/15 font-semibold" title="2 recommended connections not yet connected">⚠ 2</span>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
</span>
</button>
</div>
<!-- conversation -->
<div class="flex-1 overflow-y-auto hairline-scroll">
<div class="max-w-3xl mx-auto px-6 py-6 space-y-5">
<!-- ===== connector inbound message: SLACK (ask #1) ===== -->
<article class="id-reveal rounded-xl2 border border-line bg-panel overflow-hidden" data-brand="slack">
<header class="flex items-center gap-2 px-3.5 py-2 border-b border-line" style="background:var(--brand-soft)">
<span class="w-5 h-5 grid place-items-center">{{SLACK_ICON}}</span>
<span class="text-[12.5px] font-semibold" style="color:var(--brand)">#ocw-test</span>
<span class="text-faint">·</span>
<span class="text-[12.5px] font-medium">Jordan Lee</span>
<span class="text-[11px] text-faint id-name ml-0.5">via Slack</span>
<span class="text-[11px] text-faint font-mono id-hidden ml-0.5">C0BD7KZ1AH5 · U07JK68S4BH</span>
<span class="ml-auto text-[11px] text-faint">2:14 PM</span>
</header>
<div class="px-3.5 py-2.5 text-[14.5px] leading-relaxed">
Hey team — can someone confirm the staging deploy actually went out? Dashboard still shows the old build.
</div>
</article>
<!-- assistant -->
<div>
<div class="text-[11px] tracking-[0.06em] uppercase text-faint mb-1">Assistant</div>
<div class="text-[15px] leading-relaxed">
I checked the deploy log in the workspace — the staging release <span class="font-mono text-[13px] px-1 rounded bg-paper border border-line">v2.4.1</span> finished at 2:09 PM. The dashboard cache likely hasn't refreshed. I'll post a note back in the channel and pin the release notes.
</div>
</div>
<!-- tool actions + approvals collapsed by default — cleaner thread, expand for detail -->
<details class="rounded-lg border border-line bg-panel/50">
<summary class="flex items-center gap-2 px-3 py-2 cursor-pointer select-none">
<svg class="chev transition-transform text-faint" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M9 6l6 6-6 6"/></svg>
<span class="text-[12.5px] text-muted">2 actions · <span class="text-ok font-medium">1 approval ✓</span></span>
<span class="ml-auto text-[11px] text-faint when-closed">show details</span>
</summary>
<div class="px-2 pb-2 pt-1 space-y-2 border-t border-line">
<!-- tool call with args (card now shows what) -->
<div class="rounded-lg border border-line bg-panel">
<div class="flex items-center gap-2 px-3 py-2">
<span class="w-5 h-5 rounded grid place-items-center bg-tealSoft text-tealInk">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M4 12h16M4 17h10"/></svg>
</span>
<span class="font-mono text-[12.5px]">send_message</span>
<span class="font-mono text-[11.5px] text-faint truncate">target: slack:#ocw-test · text: "staging v2.4.1 is live (2:09 PM)…"</span>
<span class="ml-auto text-[11px] px-1.5 py-0.5 rounded bg-okSoft text-ok border border-okLine">done</span>
</div>
</div>
<!-- approval mirrored to Slack (resolved) -->
<div class="rounded-lg border border-tealLine bg-tealSoft/50">
<div class="flex items-center gap-2 px-3 py-2">
<span class="w-5 h-5 grid place-items-center" data-brand="slack">{{SLACK_ICON}}</span>
<span class="text-[13px]"><span class="font-mono">write_file</span> approval</span>
<span class="text-[11.5px] text-muted">· sent to <span class="font-medium">#ocw-approvals</span></span>
<span class="ml-auto inline-flex items-center gap-1 text-[11.5px] text-ok font-medium">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M5 13l4 4L19 7"/></svg>
Approved by Rohit in Slack
</span>
</div>
<div class="px-3 pb-2 -mt-0.5 font-mono text-[11.5px] text-muted">path: runbook-notes.md · content: cache TTL 5m; pin v2.4.1 notes…</div>
</div>
</div>
</details>
<!-- ===== a DIFFERENT connector inbound (Salesforce) — shows the pattern scales ===== -->
<article class="id-reveal rounded-xl2 border border-line bg-panel overflow-hidden" data-brand="salesforce">
<header class="flex items-center gap-2 px-3.5 py-2 border-b border-line" style="background:var(--brand-soft)">
<span class="w-5 h-5 grid place-items-center">{{SF_ICON}}</span>
<span class="text-[12.5px] font-semibold" style="color:var(--brand)">Case #00123</span>
<span class="text-faint">·</span>
<span class="text-[12.5px] font-medium">Dana Whitfield</span>
<span class="text-[11px] text-faint ml-0.5">via Salesforce</span>
<span class="ml-auto text-[11px] text-faint">2:21 PM</span>
</header>
<div class="px-3.5 py-2.5 text-[14.5px] leading-relaxed">
New comment on the escalation: customer confirms the workaround held overnight. OK to downgrade severity?
</div>
</article>
<!-- your message -->
<div class="flex justify-end">
<div class="max-w-[78%] px-3.5 py-2.5 rounded-[14px_14px_4px_14px] bg-solid text-onSolid text-[14.5px] leading-relaxed">
Reply in #ocw-test that the cache should clear in ~5 min, and ask Dana for the latest log before we downgrade.
</div>
</div>
</div>
</div>
<!-- composer -->
<div class="px-6 pb-5 pt-1">
<div class="max-w-3xl mx-auto rounded-2xl border border-line bg-panel shadow-sm">
<div class="flex items-center gap-2 px-3 pt-2.5">
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-paper border border-line text-[12px] text-muted">📁 1 directory</span>
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-accentSoft border border-accent/30 text-[12px] text-accent font-medium">● Unattended</span>
<span class="ml-auto inline-flex items-center gap-1 px-2 py-1 rounded-md bg-paper border border-line text-[12px] text-muted">claude-opus-4-8 ▾</span>
</div>
<div class="px-3.5 py-3 text-[14.5px] text-faint">Message the coworker…</div>
<div class="flex items-center gap-2 px-3 pb-2.5">
<button class="text-faint hover:text-ink"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg></button>
<span class="text-[12px] text-muted">Ask for approval ▾</span>
<button class="ml-auto w-8 h-8 rounded-full bg-accent text-white grid place-items-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
</button>
</div>
</div>
</div>
</main>
<!-- ========================= MAIN: INTEGRATIONS VIEW ========================= -->
<main id="view-integrations" class="flex-1 min-w-0 flex bg-paper hidden">
<!-- sub-nav (fixes the "everything dumped in one scroll") -->
<nav class="w-[208px] shrink-0 border-r border-line bg-panel/40 px-3 py-4">
<div class="px-2 text-[13.5px] font-semibold mb-3 flex items-center gap-2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M9 7V4a1 1 0 011-1h4a1 1 0 011 1v3"/><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M3 12h18"/></svg>
Integrations
</div>
<button data-tab="connectors" class="int-tab w-full text-left px-2.5 py-2 rounded-lg text-[13px] flex items-center justify-between">
<span class="flex items-center gap-2"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M6 3v6a6 6 0 0012 0V3M9 21v-4M15 21v-4"/></svg> Connectors</span>
<span class="text-[11px] text-faint">3</span>
</button>
<button data-tab="messaging" class="int-tab w-full text-left px-2.5 py-2 rounded-lg text-[13px] flex items-center gap-2">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg> Messaging routing
</button>
<button data-tab="activity" class="int-tab w-full text-left px-2.5 py-2 rounded-lg text-[13px] flex items-center justify-between">
<span class="flex items-center gap-2"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg> Activity</span>
<span class="text-[11px] text-white bg-warnInk/90 rounded-full px-1.5 leading-[15px]">2</span>
</button>
<button data-tab="mcp" class="int-tab w-full text-left px-2.5 py-2 rounded-lg text-[13px] flex items-center gap-2">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18"/></svg> MCP servers
</button>
</nav>
<div class="flex-1 min-w-0 overflow-y-auto hairline-scroll">
<div class="max-w-4xl mx-auto px-7 py-6">
<!-- ---- Connectors ---- -->
<section data-panel="connectors">
<div class="flex items-end justify-between mb-4">
<div>
<h2 class="text-[18px] font-semibold tracking-tight">Connectors</h2>
<p class="text-[12.5px] text-muted mt-0.5">Apps and tools OpenCoworker can use. You bring the credentials for this local build.</p>
</div>
<button class="text-[12.5px] px-3 py-1.5 rounded-lg border border-line bg-panel hover:border-lineStrong">Browse all</button>
</div>
<div class="grid grid-cols-2 gap-3">
<!-- Slack (connected, expandable) -->
<div class="col-span-2 rounded-xl2 border border-line bg-panel" data-brand="slack">
<div class="flex items-center gap-3 p-3.5">
<span class="w-9 h-9 rounded-lg grid place-items-center" style="background:var(--brand-soft)">{{SLACK_ICON}}</span>
<div class="min-w-0">
<div class="flex items-center gap-2"><span class="font-semibold text-[14px]">Slack</span><span class="text-[10.5px] px-1.5 py-0.5 rounded border border-line text-muted">two-way</span></div>
<div class="text-[12px] text-muted flex items-center gap-1.5"><span class="w-1.5 h-1.5 rounded-full bg-ok"></span> deeplearning.ai / ocw · 1 allowed</div>
</div>
<div class="ml-auto flex items-center gap-2">
<button class="text-[12.5px] px-3 py-1.5 rounded-lg border border-line bg-paper hover:border-lineStrong">Settings</button>
<button class="text-[12.5px] text-danger/80 hover:text-danger">Disconnect</button>
</div>
</div>
<!-- expanded: allow-list + recent senders (the Fix-3 surface, refined) -->
<div class="border-t border-line px-3.5 py-3 grid grid-cols-2 gap-5">
<div>
<div class="text-[11px] uppercase tracking-[0.05em] text-faint font-semibold mb-2">Allowed to message</div>
<div class="flex flex-wrap gap-1.5">
<span class="inline-flex items-center gap-1.5 pl-2 pr-1 py-1 rounded-full bg-paper border border-line text-[12px]">
<span class="w-4 h-4 rounded-full bg-accentSoft text-accent grid place-items-center text-[9px] font-bold">RP</span>
Rohit Prasad
<button class="w-4 h-4 grid place-items-center text-faint hover:text-danger">×</button>
</span>
</div>
</div>
<div>
<div class="text-[11px] uppercase tracking-[0.05em] text-faint font-semibold mb-2">Recent senders</div>
<div class="space-y-1.5">
<div class="flex items-center gap-2 text-[12.5px]">
<span class="w-5 h-5 rounded-full bg-paper border border-line grid place-items-center text-[9px] font-bold text-muted">JL</span>
<span>Jordan Lee <span class="text-faint">· channel</span></span>
<button class="ml-auto text-[11.5px] px-2 py-0.5 rounded-md bg-accent text-white">Allow</button>
</div>
</div>
</div>
</div>
</div>
<!-- Salesforce (available) -->
<div class="rounded-xl2 border border-line bg-panel p-3.5 flex items-center gap-3" data-brand="salesforce">
<span class="w-9 h-9 rounded-lg grid place-items-center" style="background:var(--brand-soft)">{{SF_ICON}}</span>
<div class="min-w-0">
<div class="font-semibold text-[14px]">Salesforce</div>
<div class="text-[12px] text-muted">Cases, comments, records</div>
</div>
<button class="ml-auto text-[12.5px] px-3 py-1.5 rounded-lg border border-line bg-paper hover:border-lineStrong">Connect</button>
</div>
<!-- HubSpot (available) -->
<div class="rounded-xl2 border border-line bg-panel p-3.5 flex items-center gap-3" data-brand="hubspot">
<span class="w-9 h-9 rounded-lg grid place-items-center" style="background:var(--brand-soft)">{{HS_ICON}}</span>
<div class="min-w-0">
<div class="font-semibold text-[14px]">HubSpot</div>
<div class="text-[12px] text-muted">CRM, tickets, contacts</div>
</div>
<button class="ml-auto text-[12.5px] px-3 py-1.5 rounded-lg border border-line bg-paper hover:border-lineStrong">Connect</button>
</div>
<!-- Telegram -->
<div class="rounded-xl2 border border-line bg-panel p-3.5 flex items-center gap-3" data-brand="telegram">
<span class="w-9 h-9 rounded-lg grid place-items-center" style="background:var(--brand-soft)">{{TG_ICON}}</span>
<div class="min-w-0">
<div class="font-semibold text-[14px]">Telegram</div>
<div class="text-[12px] text-muted">Two-way messaging</div>
</div>
<button class="ml-auto text-[12.5px] px-3 py-1.5 rounded-lg border border-line bg-paper hover:border-lineStrong">Connect</button>
</div>
</div>
</section>
<!-- ---- Messaging routing (groups subscriptions + DM route + approvals route) ---- -->
<section data-panel="messaging" class="hidden">
<div class="mb-4">
<h2 class="text-[18px] font-semibold tracking-tight">Messaging routing</h2>
<p class="text-[12.5px] text-muted mt-0.5">How inbound messages reach sessions, and where Unattended approvals go out.</p>
</div>
<!-- inbound: channel subscriptions -->
<div class="rounded-xl2 border border-line bg-panel mb-4">
<div class="px-4 py-3 border-b border-line flex items-center gap-2">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="text-muted"><path d="M12 2v20M2 12h20" transform="rotate(20 12 12)"/></svg>
<span class="font-semibold text-[13.5px]">Channel subscriptions</span>
<span class="text-[12px] text-muted">— sessions that listen to a channel (inbound)</span>
<button class="ml-auto text-[12px] px-2.5 py-1 rounded-md bg-accent text-white">+ Subscribe</button>
</div>
<table class="w-full text-[13px]">
<thead class="text-[11px] uppercase tracking-[0.04em] text-faint">
<tr class="text-left"><th class="font-medium px-4 py-2">Session</th><th class="font-medium px-4 py-2">Listens to</th><th class="font-medium px-4 py-2">Inbox routes to</th><th></th></tr>
</thead>
<tbody>
<tr class="border-t border-line">
<td class="px-4 py-2.5">hi</td>
<td class="px-4 py-2.5"><span class="inline-flex items-center gap-1.5" data-brand="slack"><span class="w-4 h-4">{{SLACK_ICON}}</span> #ocw-test</span></td>
<td class="px-4 py-2.5"><span class="inline-flex items-center gap-1.5 text-muted" data-brand="slack"><span class="w-4 h-4">{{SLACK_ICON}}</span> #ocw-approvals</span></td>
<td class="px-4 py-2.5 text-right"><button class="text-faint hover:text-danger">×</button></td>
</tr>
</tbody>
</table>
</div>
<div class="grid grid-cols-2 gap-4">
<!-- DM routing -->
<div class="rounded-xl2 border border-line bg-panel p-4">
<div class="font-semibold text-[13.5px] mb-1">Direct messages</div>
<p class="text-[12px] text-muted mb-3">Session that handles DMs to the bot. Unrouted DMs are parked under Activity.</p>
<div class="flex items-center gap-2">
<span class="w-5 h-5" data-brand="slack">{{SLACK_ICON}}</span>
<select class="flex-1 px-2.5 py-1.5 rounded-lg border border-line bg-paper text-[13px]"><option>hi</option><option>No session — park DMs</option></select>
</div>
</div>
<!-- approvals routing -->
<div class="rounded-xl2 border border-line bg-panel p-4">
<div class="font-semibold text-[13.5px] mb-1">Unattended approvals</div>
<p class="text-[12px] text-muted mb-3">Channel where an Unattended session posts Approve/Deny buttons.</p>
<div class="flex items-center gap-2">
<span class="w-5 h-5" data-brand="slack">{{SLACK_ICON}}</span>
<select class="flex-1 px-2.5 py-1.5 rounded-lg border border-line bg-paper text-[13px]"><option>#ocw-approvals</option><option>In-app Inbox only</option></select>
</div>
</div>
</div>
</section>
<!-- ---- Activity / diagnostics (unrouted + failed) ---- -->
<section data-panel="activity" class="hidden">
<div class="mb-4">
<h2 class="text-[18px] font-semibold tracking-tight">Activity</h2>
<p class="text-[12.5px] text-muted mt-0.5">Unrouted inbound and background-turn failures — nothing vanishes silently.</p>
</div>
<div class="rounded-xl2 border border-line bg-panel overflow-hidden">
<table class="w-full text-[13px]">
<thead class="text-[11px] uppercase tracking-[0.04em] text-faint">
<tr class="text-left"><th class="font-medium px-4 py-2">When</th><th class="font-medium px-4 py-2">Source</th><th class="font-medium px-4 py-2">Reason</th><th class="font-medium px-4 py-2">Message</th></tr>
</thead>
<tbody>
<tr class="border-t border-line">
<td class="px-4 py-2.5 text-muted whitespace-nowrap">2:31 PM</td>
<td class="px-4 py-2.5"><span class="inline-flex items-center gap-1.5" data-brand="slack"><span class="w-4 h-4">{{SLACK_ICON}}</span> DM · casey</span></td>
<td class="px-4 py-2.5"><span class="text-warnInk">no DM session designated</span></td>
<td class="px-4 py-2.5 text-muted truncate max-w-[16rem]">hey, are you around? quick q…</td>
</tr>
<tr class="border-t border-line">
<td class="px-4 py-2.5 text-muted whitespace-nowrap">2:28 PM</td>
<td class="px-4 py-2.5 font-mono text-[12px]">session 856b16f8</td>
<td class="px-4 py-2.5"><span class="text-danger">401 · invalid model auth</span></td>
<td class="px-4 py-2.5 text-muted truncate max-w-[16rem]">💬 New message on #ocw-test…</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- ---- MCP ---- -->
<section data-panel="mcp" class="hidden">
<div class="mb-4">
<h2 class="text-[18px] font-semibold tracking-tight">MCP servers</h2>
<p class="text-[12.5px] text-muted mt-0.5">Model Context Protocol tool servers.</p>
</div>
<div class="rounded-xl2 border border-line bg-panel p-4 text-[13px] text-muted">No MCP servers configured. <button class="text-accent font-medium">Add a server</button></div>
</section>
</div>
</div>
</main>
<!-- ============================ MAIN: PERSONA DETAIL ============================ -->
<main id="view-persona" class="flex-1 min-w-0 flex flex-col bg-paper hidden">
<div class="h-12 shrink-0 px-5 flex items-center gap-3 border-b border-line bg-panel/70 backdrop-blur">
<button id="personaBack" class="inline-flex items-center gap-1 text-[12.5px] text-muted hover:text-ink">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M15 18l-6-6 6-6"/></svg> Back
</button>
<span class="text-faint">·</span>
<span class="text-[13px] font-semibold">Persona</span>
</div>
<div class="flex-1 overflow-y-auto hairline-scroll">
<div class="max-w-3xl mx-auto px-7 py-6 space-y-6">
<!-- identity -->
<div class="flex items-start gap-3.5">
<span class="w-12 h-12 rounded-xl2 bg-panel border border-line grid place-items-center text-[22px]">🛠️</span>
<div class="min-w-0">
<h1 class="text-[20px] font-semibold tracking-tight">Ops Coworker</h1>
<p class="text-[13px] text-muted mt-0.5">Operate and investigate — runbooks, logs, infrastructure</p>
</div>
<div class="ml-auto flex items-center gap-2">
<span class="text-[12px] text-muted">Enabled</span>
<span class="tgl" role="switch" aria-checked="true" tabindex="0" title="Enable this persona"><span class="knob"></span></span>
</div>
</div>
<!-- about -->
<section>
<div class="text-[11px] uppercase tracking-[0.05em] text-faint font-semibold mb-1.5">About</div>
<p class="text-[14px] leading-relaxed text-ink/90">A careful, methodical operations engineer. It investigates incidents, runs runbooks, inspects logs and metrics, and produces clear operational deliverables — incident notes, postmortems, runbook updates — preferring read-only, reversible steps and asking before anything consequential.</p>
</section>
<!-- tools -->
<section>
<div class="text-[11px] uppercase tracking-[0.05em] text-faint font-semibold mb-2">Built-in capabilities</div>
<div class="flex flex-wrap gap-1.5">
<span class="px-2 py-1 rounded-md bg-panel border border-line text-[12px] font-mono">files</span>
<span class="px-2 py-1 rounded-md bg-panel border border-line text-[12px] font-mono">search</span>
<span class="px-2 py-1 rounded-md bg-panel border border-line text-[12px] font-mono">shell</span>
<span class="px-2 py-1 rounded-md bg-panel border border-line text-[12px] font-mono">todo</span>
</div>
</section>
<!-- connections for full benefit (from manifest `recommends`) -->
<section>
<div class="text-[11px] uppercase tracking-[0.05em] text-faint font-semibold mb-1">Connections for full benefit</div>
<p class="text-[12.5px] text-muted mb-2.5">Declared by the persona — wire Ops into these to unlock its full workflow.</p>
<div class="rounded-xl2 border border-line overflow-hidden">
<div class="flex items-center gap-3 p-3 bg-panel">
<span class="src-badge" data-brand="github">{{GH_ICON}}</span>
<div class="flex-1 min-w-0"><div class="flex items-center gap-2"><span class="text-[13px] font-medium">GitHub</span><span class="text-[10px] px-1.5 py-0.5 rounded-full bg-warnSoft/70 text-warnInk border border-warnInk/15">core</span></div><div class="text-[12px] text-muted">confirm deploys and inspect the PRs behind a change</div></div>
<span class="inline-flex items-center gap-1 text-[11.5px] text-ok"><span class="w-1.5 h-1.5 rounded-full bg-ok"></span>connected</span>
</div>
<div class="flex items-center gap-3 p-3 bg-panel border-t border-line">
<span class="src-badge" data-brand="slack">{{SLACK_ICON}}</span>
<div class="flex-1 min-w-0"><div class="flex items-center gap-2"><span class="text-[13px] font-medium">Slack</span><span class="text-[10px] px-1.5 py-0.5 rounded-full bg-warnSoft/70 text-warnInk border border-warnInk/15">core</span></div><div class="text-[12px] text-muted">receive alerts and reply to the team in-channel</div></div>
<span class="inline-flex items-center gap-1 text-[11.5px] text-ok"><span class="w-1.5 h-1.5 rounded-full bg-ok"></span>connected</span>
</div>
<div class="flex items-center gap-3 p-3 bg-panel border-t border-line">
<span class="src-badge" data-brand="datadog">{{DD_ICON}}</span>
<div class="flex-1 min-w-0"><div class="flex items-center gap-2"><span class="text-[13px] font-medium">Datadog</span><span class="text-[10px] px-1.5 py-0.5 rounded-full bg-warnSoft/70 text-warnInk border border-warnInk/15">core</span></div><div class="text-[12px] text-muted">pull the firing alerts and the incident timeline</div></div>
<button class="text-[12px] px-2.5 py-1.5 rounded-lg bg-accent text-white shrink-0">Connect</button>
</div>
<div class="flex items-center gap-3 p-3 bg-panel border-t border-line">
<span class="src-badge" data-brand="default">{{PLUG_ICON}}</span>
<div class="flex-1 min-w-0"><div class="text-[13px] font-medium">PagerDuty</div><div class="text-[12px] text-muted">see who's on-call before paging</div></div>
<button class="text-[12px] px-2.5 py-1.5 rounded-lg border border-line bg-paper hover:border-lineStrong shrink-0">Connect</button>
</div>
<div class="flex items-center gap-3 p-3 bg-panel border-t border-line">
<span class="src-badge" data-brand="mcp">{{PLUG_ICON}}</span>
<div class="flex-1 min-w-0"><div class="flex items-center gap-2"><span class="text-[13px] font-medium">Filesystem</span><span class="text-[10px] px-1.5 py-0.5 rounded border border-line text-faint">MCP</span></div><div class="text-[12px] text-muted">read runbooks and postmortems from a local folder</div></div>
<button class="text-[12px] px-2.5 py-1.5 rounded-lg border border-line bg-paper hover:border-lineStrong shrink-0">Add</button>
</div>
</div>
</section>
<!-- default session connections (persona → session default, the middle of the hierarchy) -->
<section>
<div class="text-[11px] uppercase tracking-[0.05em] text-faint font-semibold mb-1">New sessions get by default</div>
<p class="text-[12.5px] text-muted mb-2.5">When you start an Ops session these are enabled automatically. You can still mute any of them per session.</p>
<div class="space-y-1.5">
<div class="flex items-center gap-3 p-2.5 rounded-xl2 border border-line bg-panel">
<span class="src-badge" data-brand="slack">{{SLACK_ICON}}</span>
<div class="flex-1 text-[13px] font-medium">Slack</div>
<span class="tgl" role="switch" aria-checked="true" tabindex="0" title="On by default for new Ops sessions"><span class="knob"></span></span>
</div>
<div class="flex items-center gap-3 p-2.5 rounded-xl2 border border-line bg-panel">
<span class="src-badge" data-brand="github">{{GH_ICON}}</span>
<div class="flex-1 text-[13px] font-medium">GitHub</div>
<span class="tgl" role="switch" aria-checked="true" tabindex="0"><span class="knob"></span></span>
</div>
<div class="flex items-center gap-3 p-2.5 rounded-xl2 border border-line bg-panel" style="opacity:.5">
<span class="src-badge" data-brand="datadog">{{DD_ICON}}</span>
<div class="flex-1 text-[13px] font-medium">Datadog <span class="text-[11px] text-faint font-normal">· connect to enable</span></div>
<span class="tgl" role="switch" aria-checked="false" tabindex="0"><span class="knob"></span></span>
</div>
</div>
</section>
<!-- defaults -->
<section class="flex flex-wrap gap-x-8 gap-y-2 text-[12.5px]">
<div><span class="text-faint">Models</span> · <span class="font-mono">claude-opus-4-8</span>, <span class="font-mono">gpt-5.5</span></div>
<div><span class="text-faint">Default mode</span> · Interactive</div>
<div><span class="text-faint">Workspace</span> · deliverable</div>
</section>
</div>
</div>
</main>
</div>
<!-- ============ per-session Sources drawer (persona-aware connections) ============ -->
<div id="sourcesDrawer" class="fixed inset-0 z-40 hidden">
<div class="absolute inset-0 bg-black/30 backdrop-blur-[1px]" data-close></div>
<aside class="absolute right-0 top-0 h-full w-[420px] max-w-[92vw] bg-panel border-l border-line shadow-2xl flex flex-col">
<header class="px-4 h-12 shrink-0 flex items-center gap-2 border-b border-line">
<span class="w-6 h-6 rounded-md bg-panel border border-line grid place-items-center text-[13px]">🛠️</span>
<div class="min-w-0">
<div class="text-[13px] font-semibold leading-tight truncate">Ops · incident watch</div>
<div class="text-[11px] text-faint leading-tight">Session connections</div>
</div>
<button data-close class="ml-auto w-7 h-7 grid place-items-center rounded-md text-faint hover:text-ink hover:bg-paper"></button>
</header>
<div class="flex-1 overflow-y-auto hairline-scroll px-4 py-4 space-y-5">
<!-- persona blurb: what to connect & why -->
<div class="rounded-xl2 border border-accent/25 bg-accentSoft/50 p-3.5">
<div class="flex items-center gap-2 text-[13px] font-semibold"><span></span> Get the most out of Ops</div>
<p class="text-[12.5px] text-muted mt-1.5 leading-relaxed">
The <b>SRE</b> persona is built to triage incidents end-to-end. Wire it into <b>GitHub</b>,
<b>Slack</b>, and a <b>monitoring dashboard</b> so it can confirm deploys, pull firing alerts,
and reply in-channel — without you relaying. Add what you can now; finish the rest later.
</p>
<div class="mt-2 h-1.5 rounded-full bg-panel overflow-hidden"><div class="h-full bg-accent" style="width:50%"></div></div>
<div class="mt-1 text-[11px] text-faint">2 of 4 recommended connected</div>
</div>
<!-- connected for this session — each can be toggled OFF for THIS session (§4 hierarchy) -->
<section>
<div class="flex items-center justify-between mb-2">
<div class="text-[11px] uppercase tracking-[0.05em] text-faint font-semibold">Connected · 2</div>
<div class="text-[10.5px] text-faint">enabled for this session</div>
</div>
<div class="space-y-1.5">
<div class="flex items-center gap-3 p-2.5 rounded-xl2 border border-line bg-paper">
<span class="src-badge" data-brand="slack">{{SLACK_ICON}}</span>
<div class="min-w-0 flex-1">
<div class="text-[13px] font-medium">Slack</div>
<div class="text-[12px] text-muted truncate">#ocw-test · DMs → this session</div>
</div>
<span class="tgl" role="switch" aria-checked="true" tabindex="0" title="Enabled for this session — tap to mute here"><span class="knob"></span></span>
</div>
<div class="flex items-center gap-3 p-2.5 rounded-xl2 border border-line bg-paper opacity-100">
<span class="src-badge" data-brand="github">{{GH_ICON}}</span>
<div class="min-w-0 flex-1">
<div class="text-[13px] font-medium">GitHub</div>
<div class="text-[12px] text-muted truncate">deeplearning-ai/platform</div>
</div>
<span class="tgl" role="switch" aria-checked="true" tabindex="0" title="Enabled for this session"><span class="knob"></span></span>
</div>
</div>
<p class="text-[11px] text-faint mt-1.5">Turning one off mutes it for <b>this session only</b> — the connector stays connected and enabled for the persona.</p>
</section>
<!-- recommended connectors -->
<section>
<div class="text-[11px] uppercase tracking-[0.05em] text-faint font-semibold mb-2">Recommended connectors</div>
<div class="space-y-1.5">
<div class="flex items-start gap-3 p-2.5 rounded-xl2 border border-line">
<span class="src-badge" data-brand="datadog">{{DD_ICON}}</span>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2"><span class="text-[13px] font-medium">Datadog</span><span class="text-[10px] px-1.5 py-0.5 rounded-full bg-warnSoft/70 text-warnInk border border-warnInk/15">core</span></div>
<div class="text-[12px] text-muted">so I can pull the firing alerts &amp; incident timeline</div>
</div>
<button class="text-[12px] px-2.5 py-1.5 rounded-lg bg-accent text-white shrink-0">Connect</button>
</div>
<!-- a connector with NO custom logo → fallback plug + neutral brand -->
<div class="flex items-start gap-3 p-2.5 rounded-xl2 border border-line">
<span class="src-badge" data-brand="default">{{PLUG_ICON}}</span>
<div class="min-w-0 flex-1">
<div class="text-[13px] font-medium">PagerDuty</div>
<div class="text-[12px] text-muted">so I can see who's on-call before paging</div>
</div>
<button class="text-[12px] px-2.5 py-1.5 rounded-lg border border-line bg-paper hover:border-lineStrong shrink-0">Connect</button>
</div>
</div>
</section>
<!-- recommended MCP -->
<section>
<div class="text-[11px] uppercase tracking-[0.05em] text-faint font-semibold mb-2">Recommended MCP servers</div>
<div class="flex items-start gap-3 p-2.5 rounded-xl2 border border-line">
<span class="src-badge" data-brand="mcp">{{PLUG_ICON}}</span>
<div class="min-w-0 flex-1">
<div class="text-[13px] font-medium">Filesystem</div>
<div class="text-[12px] text-muted">read runbooks &amp; postmortems from a local folder</div>
</div>
<button class="text-[12px] px-2.5 py-1.5 rounded-lg border border-line bg-paper hover:border-lineStrong shrink-0">Add</button>
</div>
</section>
</div>
<footer class="px-4 py-3 border-t border-line">
<button id="toGlobalInt" class="w-full text-[12.5px] text-accent font-medium hover:underline text-left">Manage all connectors (global) →</button>
</footer>
</aside>
</div>
<script>
// brand icon SVGs, injected into the double-brace placeholders so the markup above stays readable
const ICONS = {
SLACK_ICON: `<svg viewBox="0 0 122.8 122.8" width="100%" height="100%">
<path fill="#E01E5A" d="M25.8 77.6c0 7.1-5.8 12.9-12.9 12.9S0 84.7 0 77.6s5.8-12.9 12.9-12.9h12.9v12.9z"/>
<path fill="#E01E5A" d="M32.3 77.6c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9v32.3c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V77.6z"/>
<path fill="#36C5F0" d="M45.2 25.8c-7.1 0-12.9-5.8-12.9-12.9S38.1 0 45.2 0s12.9 5.8 12.9 12.9v12.9H45.2z"/>
<path fill="#36C5F0" d="M45.2 32.3c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H12.9C5.8 58.1 0 52.3 0 45.2s5.8-12.9 12.9-12.9h32.3z"/>
<path fill="#2EB67D" d="M97 45.2c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9-5.8 12.9-12.9 12.9H97V45.2z"/>
<path fill="#2EB67D" d="M90.5 45.2c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V12.9C64.7 5.8 70.5 0 77.6 0s12.9 5.8 12.9 12.9v32.3z"/>
<path fill="#ECB22E" d="M77.6 97c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9-12.9-5.8-12.9-12.9V97h12.9z"/>
<path fill="#ECB22E" d="M77.6 90.5c-7.1 0-12.9-5.8-12.9-12.9s5.8-12.9 12.9-12.9h32.3c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H77.6z"/></svg>`,
SF_ICON: `<svg viewBox="0 0 24 24" width="100%" height="100%" fill="#00A1E0"><path d="M10 7.5a3.2 3.2 0 015.7-1 3.6 3.6 0 014.8 3.4 3.4 3.4 0 01-1.2 6.6H6.5A3.5 3.5 0 016 9.6 3.2 3.2 0 0110 7.5z"/></svg>`,
HS_ICON: `<svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="#FF7A59" stroke-width="2"><circle cx="8" cy="16" r="2.4"/><circle cx="17" cy="9" r="2.4"/><path d="M17 11.4V14M9.7 14.6l5.6-3.8M17 6.6V4"/></svg>`,
TG_ICON: `<svg viewBox="0 0 24 24" width="100%" height="100%" fill="#229ED9"><path d="M21 4L3 11l5 2 2 5 3-3 4 3 4-14z"/></svg>`,
GH_ICON: `<svg viewBox="0 0 24 24" width="100%" height="100%" fill="var(--brand)"><path d="M12 1.5A10.5 10.5 0 001.5 12c0 4.6 3 8.6 7.2 10 .5.1.7-.2.7-.5v-1.7c-2.9.6-3.5-1.4-3.5-1.4-.5-1.2-1.2-1.5-1.2-1.5-.9-.6.1-.6.1-.6 1 .1 1.6 1 1.6 1 .9 1.6 2.4 1.1 3 .9.1-.7.4-1.1.7-1.4-2.3-.3-4.8-1.2-4.8-5.2 0-1.1.4-2.1 1-2.8-.1-.3-.5-1.4.1-2.8 0 0 .9-.3 2.8 1.1a9.6 9.6 0 015 0c1.9-1.3 2.8-1.1 2.8-1.1.6 1.4.2 2.5.1 2.8.7.7 1 1.7 1 2.8 0 4-2.5 4.9-4.8 5.2.4.3.7 1 .7 2v3c0 .3.2.6.7.5 4.2-1.4 7.2-5.4 7.2-10A10.5 10.5 0 0012 1.5z"/></svg>`,
DD_ICON: `<svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="#632ca6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h3l2 6 4-14 2 8 2-3h5"/></svg>`,
PLUG_ICON: `<svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="var(--brand)" stroke-width="1.8" stroke-linecap="round"><path d="M9 7V3M15 7V3M7 7h10v4a5 5 0 01-10 0V7zM12 16v5"/></svg>`,
};
// Connector registry — single source of truth for brand color + logo, with a fallback so an
// unknown/custom connector still renders cleanly. (In the app this lives in the connector
// descriptor; the mock mirrors the shape.)
window.CONNECTORS = {
slack: { label: 'Slack', icon: 'SLACK_ICON' },
salesforce: { label: 'Salesforce', icon: 'SF_ICON' },
hubspot: { label: 'HubSpot', icon: 'HS_ICON' },
telegram: { label: 'Telegram', icon: 'TG_ICON' },
github: { label: 'GitHub', icon: 'GH_ICON' },
datadog: { label: 'Datadog', icon: 'DD_ICON' },
default: { label: 'Connector', icon: 'PLUG_ICON' }, // fallback
};
document.body.innerHTML = document.body.innerHTML.replace(/\{\{(\w+)\}\}/g, (m, k) => ICONS[k] || m);
// view switch (session / integrations / persona)
const views = {
session: document.getElementById('view-session'),
integrations: document.getElementById('view-integrations'),
persona: document.getElementById('view-persona'),
};
function setView(v){
Object.entries(views).forEach(([k, el]) => el.classList.toggle('hidden', k !== v));
document.querySelectorAll('.view-tab').forEach(b => {
const on = b.dataset.view === v;
b.classList.toggle('bg-panel', on); b.classList.toggle('shadow-sm', on);
b.classList.toggle('text-ink', on); b.classList.toggle('text-muted', !on);
});
}
document.querySelectorAll('[data-view]').forEach(b => b.addEventListener('click', () => setView(b.dataset.view)));
// persona detail page: opened from a persona-group gear or "Manage personas…", back returns to session
document.querySelectorAll('.persona-gear').forEach(g => g.addEventListener('click', () => setView('persona')));
document.getElementById('personaBack').addEventListener('click', () => setView('session'));
document.getElementById('managePersonas').addEventListener('click', () => { document.getElementById('personaMenu').classList.add('hidden'); setView('persona'); });
// integrations sub-tabs
function setTab(t){
document.querySelectorAll('[data-panel]').forEach(p => p.classList.toggle('hidden', p.dataset.panel !== t));
document.querySelectorAll('.int-tab').forEach(b => {
const on = b.dataset.tab === t;
b.classList.toggle('bg-accentSoft', on); b.classList.toggle('text-accent', on);
b.classList.toggle('font-medium', on); b.classList.toggle('hover:bg-paper', !on);
});
}
document.querySelectorAll('.int-tab').forEach(b => b.addEventListener('click', () => setTab(b.dataset.tab)));
// theme
document.getElementById('themeBtn').addEventListener('click', () => {
const html = document.documentElement;
html.dataset.theme = html.dataset.theme === 'dark' ? 'light' : 'dark';
});
// per-session Sources drawer (persona-aware connections)
const drawer = document.getElementById('sourcesDrawer');
document.getElementById('openSources').addEventListener('click', () => drawer.classList.remove('hidden'));
drawer.querySelectorAll('[data-close]').forEach(el => el.addEventListener('click', () => drawer.classList.add('hidden')));
document.getElementById('toGlobalInt').addEventListener('click', () => { drawer.classList.add('hidden'); setView('integrations'); });
// left-nav layout toggle: flat (Pinned/Recent) ↔ group by persona
const layoutBtn = document.getElementById('layoutToggle');
let grouped = false;
layoutBtn.addEventListener('click', () => {
grouped = !grouped;
document.getElementById('list-flat').classList.toggle('hidden', grouped);
document.getElementById('list-grouped').classList.toggle('hidden', !grouped);
document.getElementById('layoutIcon').innerHTML = grouped
? '<rect x="3" y="4" width="18" height="6" rx="1.5"/><rect x="3" y="14" width="18" height="6" rx="1.5"/>'
: '<path d="M3 5h18M3 12h18M3 19h18"/>';
layoutBtn.title = grouped ? 'Grouped by persona — tap for flat' : 'Flat — tap to group by persona';
});
// new-session persona picker
const pBtn = document.getElementById('personaMenuBtn'), pMenu = document.getElementById('personaMenu');
pBtn.addEventListener('click', (e) => { e.stopPropagation(); pMenu.classList.toggle('hidden'); });
document.addEventListener('click', (e) => { if (!pMenu.contains(e.target) && !pBtn.contains(e.target)) pMenu.classList.add('hidden'); });
// per-session connection enable/disable toggles (mute a connector for THIS session)
document.querySelectorAll('.tgl').forEach(t => {
const row = t.closest('div.rounded-xl2');
const flip = () => {
const on = t.getAttribute('aria-checked') === 'true';
t.setAttribute('aria-checked', on ? 'false' : 'true');
if (row) row.style.opacity = on ? '0.5' : '1';
};
t.addEventListener('click', flip);
t.addEventListener('keydown', (e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); flip(); } });
});
setView('session');
setTab('connectors');
</script>
</body>
</html>

View File

@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>§37 — Voice input composer states</title>
<style>
:root{
--paper:#f6f5f2;--panel:#fff;--line:#e4e2dc;--ink:#2c2c2a;--muted:#6f6e68;
--faint:#a3a19a;--accent:#3670b2;--accentSoft:#e7f0fa;--danger:#bd4f4f;
--dangerSoft:#f9e8e7;--shadow:0 8px 26px rgba(33,31,25,.08);
}
@media(prefers-color-scheme:dark){:root{
--paper:#181817;--panel:#292928;--line:#41403c;--ink:#f0eee9;--muted:#b0ada5;
--faint:#77756f;--accent:#75aade;--accentSoft:#293b4e;--danger:#ee8580;
--dangerSoft:#442a2a;--shadow:0 10px 30px rgba(0,0,0,.28);
}}
*{box-sizing:border-box;margin:0}
body{background:var(--paper);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:22px}
button{font:inherit;color:inherit}
.intro{max-width:900px;margin:0 auto 16px;color:var(--muted);font-size:13px}
.intro b{color:var(--ink)}.intro ul{margin:7px 0 0 18px;display:grid;gap:3px}
.sequence{max-width:900px;margin:auto;display:flex;align-items:center;gap:6px;color:var(--faint);font-size:11.5px;margin-bottom:20px}
.sequence span{border:1px solid var(--line);background:var(--panel);border-radius:999px;padding:5px 9px;color:var(--muted)}
.sequence i{font-style:normal}
.state{max-width:900px;margin:0 auto 25px}
.state-head{display:flex;align-items:baseline;gap:9px;margin:0 4px 8px}
.num{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;background:var(--accentSoft);color:var(--accent);font-size:11.5px;font-weight:700}
h2{font-size:13px;font-weight:650}.explain{font-size:11.5px;color:var(--faint);margin-left:auto}
.composer{background:var(--panel);border:1px solid var(--line);border-radius:17px;padding:14px 15px 10px;box-shadow:var(--shadow);min-height:132px;display:flex;flex-direction:column}
.draft{font-size:15px;line-height:1.5;min-height:64px;outline:0}.placeholder{color:var(--faint)}
.provisional{color:var(--muted);position:relative;display:inline-block}
.provisional:after{content:"";position:absolute;left:0;right:0;bottom:-2px;height:1px;background:linear-gradient(90deg,transparent,var(--accent),transparent);animation:scan 1.15s ease-in-out infinite}
@keyframes scan{0%{transform:scaleX(.12);opacity:.25}50%{transform:scaleX(1);opacity:1}100%{transform:scaleX(.12);opacity:.25}}
.caret{display:inline-block;width:1px;height:18px;background:var(--ink);vertical-align:-3px;animation:blink 1s steps(1) infinite}
@keyframes blink{50%{opacity:0}}
.row{display:flex;align-items:center;gap:7px;min-height:34px}
.icon-btn{width:30px;height:30px;border:0;background:transparent;border-radius:8px;display:grid;place-items:center;color:var(--muted);position:relative;cursor:pointer}
.icon-btn:hover{background:var(--paper);color:var(--ink)}
.plus{font-size:21px;font-weight:300}.mode{border:0;background:transparent;color:var(--muted);padding:6px 8px;border-radius:8px;font-size:12.5px}.mode:hover{background:var(--paper);color:var(--ink)}
.model{font-size:12px;color:var(--muted);padding:5px 7px}.spacer{flex:1}
.send{border:0;background:var(--ink);color:var(--panel);width:31px;height:31px;border-radius:50%;display:grid;place-items:center;font-size:18px}
.send.idle{background:var(--paper);color:var(--faint)}.send.protected{background:var(--paper);color:var(--faint);cursor:default}
.mic svg,.busy svg{width:17px;height:17px;display:block}.mic{color:var(--accent)}
.mic.recording{background:var(--dangerSoft);color:var(--danger)}
.busy{background:var(--accentSoft);color:var(--accent)}
.busy:after{content:"";position:absolute;inset:-3px;border:1.5px solid var(--accent);border-right-color:transparent;border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.listening-row{gap:10px}.wave-line{height:1px;border-top:1px dashed var(--line);flex:1;position:relative;display:flex;align-items:center;justify-content:flex-end}
.wave{height:31px;display:flex;gap:2px;align-items:center;padding-left:10px;background:var(--panel)}
.wave i{display:block;width:2px;background:var(--accent);border-radius:2px;animation:wave .72s ease-in-out infinite alternate}
.wave i:nth-child(2n){animation-delay:-.28s}.wave i:nth-child(3n){animation-delay:-.5s}.wave i:nth-child(5n){animation-delay:-.12s}
@keyframes wave{from{height:5px;opacity:.55}to{height:29px;opacity:1}}
.timer{font-variant-numeric:tabular-nums;color:var(--muted);font-size:12.5px}
.stop{width:31px;height:31px;border:0;border-radius:50%;background:var(--dangerSoft);display:grid;place-items:center;color:var(--danger)}
.stop:before{content:"";width:8px;height:8px;background:currentColor;border-radius:2px}
.busy-copy{font-size:11.5px;color:var(--accent);margin-right:2px}
.foot{max-width:900px;margin:4px auto 0;color:var(--faint);font-size:11.5px;border-top:1px solid var(--line);padding-top:12px}
@media(max-width:680px){body{padding:12px}.explain{display:none}.sequence{flex-wrap:wrap}.model{display:none}.composer{min-height:120px}.wave{padding-left:4px}.wave i:nth-child(n+14){display:none}}
</style>
</head>
<body>
<div class="intro">
<b>§37 — four composer states for local voice input.</b> The references establish the useful rhythm; this mock translates it into OpenCoworkers existing composer controls.
<ul>
<li>The live waveform means <b>the microphone is listening</b>, not that inference is already complete.</li>
<li>Transcribing is intentionally quiet because it will often last less than a second.</li>
<li>The transcript always lands as a normal editable draft. Nothing sends automatically.</li>
<li><b>Conversation invariant:</b> these are mid-conversation states, so no model name or model picker appears in the composer. OpenCoworker fixes the model after the first turn; the model picker exists only on a fresh conversation and the fixed model is shown in the conversation header.</li>
</ul>
</div>
<div class="sequence"><span>Ready</span><i>→ click mic →</i><span>Listening</span><i>→ stop →</i><span>Transcribing</span><i></i><span>Draft inserted</span></div>
<section class="state">
<div class="state-head"><span class="num">1</span><h2>Ready at rest</h2><span class="explain">Voice Input is configured; mic is available in the normal row.</span></div>
<div class="composer">
<div class="draft placeholder">Ask OpenCoworker anything…</div>
<div class="row">
<button class="icon-btn plus" aria-label="Attach"></button><button class="mode">Ask for approval⌄</button>
<span class="spacer"></span>
<button class="icon-btn mic" aria-label="Start voice input">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="8" y="3" width="8" height="13" rx="4"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3M9 21h6"/></svg>
</button><button class="send idle" aria-label="Send"></button>
</div>
</div>
</section>
<section class="state">
<div class="state-head"><span class="num">2</span><h2>Listening</h2><span class="explain">Waveform + elapsed time confirm capture; Stop is unmistakable.</span></div>
<div class="composer">
<div class="draft placeholder">Speak now…</div>
<div class="row listening-row">
<button class="icon-btn plus" aria-label="Attach"></button>
<div class="wave-line" aria-hidden="true"><div class="wave">
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
</div></div>
<span class="timer">0:05</span><button class="stop" aria-label="Stop recording"></button><button class="send idle" aria-label="Send" disabled></button>
</div>
</div>
</section>
<section class="state">
<div class="state-head"><span class="num">3</span><h2>Transcribing</h2><span class="explain">Usually fleeting; provisional text can appear, but Send waits.</span></div>
<div class="composer" aria-busy="true">
<div class="draft"><span class="provisional">Hey, whats going on?</span></div>
<div class="row">
<button class="icon-btn plus" aria-label="Attach"></button><button class="mode">Ask for approval⌄</button>
<span class="spacer"></span><span class="busy-copy" role="status" aria-live="polite">Transcribing…</span>
<button class="icon-btn busy" aria-label="Transcribing" aria-disabled="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="8" y="3" width="8" height="13" rx="4"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3M9 21h6"/></svg>
</button><button class="send protected" aria-label="Send unavailable while transcribing" disabled></button>
</div>
</div>
</section>
<section class="state">
<div class="state-head"><span class="num">4</span><h2>Draft inserted</h2><span class="explain">Ordinary editable text; mic resets; the user edits or sends.</span></div>
<div class="composer">
<div class="draft" contenteditable="true" role="textbox" aria-label="Message draft">Hey, whats going on?<span class="caret" aria-hidden="true"></span></div>
<div class="row">
<button class="icon-btn plus" aria-label="Attach"></button><button class="mode">Ask for approval⌄</button>
<span class="spacer"></span>
<button class="icon-btn mic" aria-label="Start voice input">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="8" y="3" width="8" height="13" rx="4"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3M9 21h6"/></svg>
</button><button class="send" aria-label="Send"></button>
</div>
</div>
</section>
<div class="foot"><b>Do not add a model control to these states.</b> The listening waveform is visual feedback only; the accessible state is announced separately. Existing draft text is preserved and the final transcript appends at the cursor/end according to the composer insertion rule.</div>
</body>
</html>

View File

@@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>§37 — Voice input setup lives in Settings</title>
<style>
:root{
--paper:#f6f5f2;--panel:#fff;--panel2:#fbfaf8;--line:#e4e2dc;--line2:#d5d2ca;
--ink:#2c2c2a;--muted:#6f6e68;--faint:#a3a19a;--accent:#3670b2;--accentSoft:#e7f0fa;
--ok:#2e7d4f;--okSoft:#e5f2e9;--warn:#9b6418;--warnSoft:#f8ecd9;--danger:#b34c4c;
}
@media(prefers-color-scheme:dark){:root{
--paper:#191918;--panel:#232322;--panel2:#1f1f1e;--line:#383733;--line2:#484640;
--ink:#e8e6e1;--muted:#aaa79f;--faint:#74726c;--accent:#71a8df;--accentSoft:#253649;
--ok:#67bd8b;--okSoft:#20372a;--warn:#ddb066;--warnSoft:#3b301d;--danger:#df7b7b;
}}
*{box-sizing:border-box;margin:0}
body{background:var(--paper);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:20px}
button{font:inherit;color:inherit}
.caption{max-width:1120px;margin:0 auto 14px;color:var(--muted);font-size:13px}
.caption b{color:var(--ink)}
.stage{max-width:1120px;min-height:720px;margin:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;overflow:hidden;display:grid;grid-template-columns:210px 1fr;box-shadow:0 12px 34px rgba(30,30,25,.06)}
.nav{background:color-mix(in srgb,var(--panel) 65%,var(--paper));border-right:1px solid var(--line);padding:18px 12px}
.brand{font-weight:650;font-size:13.5px;padding:0 10px 12px;display:flex;gap:8px;align-items:center}
.nav button{width:100%;border:0;background:transparent;text-align:left;padding:9px 10px;border-radius:8px;color:var(--muted);display:flex;gap:9px;align-items:center;font-size:13px}
.nav button.active{background:var(--paper);color:var(--accent);font-weight:600}
.ico{width:18px;text-align:center;font-size:14px}
main{overflow:auto;padding:26px 32px 32px}.main-inner{max-width:760px;margin:auto}
h1{font-size:21px;line-height:1.2;margin-bottom:5px}.sub{color:var(--muted);font-size:13px;margin-bottom:20px;max-width:650px}
.states{display:inline-flex;background:var(--paper);border:1px solid var(--line);border-radius:10px;padding:3px;margin-bottom:16px}
.states button{border:0;background:transparent;color:var(--muted);padding:6px 11px;border-radius:7px;font-size:12px;cursor:pointer}
.states button.active{background:var(--panel);color:var(--ink);box-shadow:0 1px 3px rgba(0,0,0,.08)}
.screen{display:none}.screen.active{display:block}
.card{background:var(--panel);border:1px solid var(--line);border-radius:13px;margin-bottom:13px;overflow:hidden}
.row{padding:14px 16px;display:flex;align-items:center;gap:12px}.row+.row{border-top:1px solid var(--line)}
.row-main{min-width:0;flex:1}.title{font-weight:620;font-size:13.5px}.help{font-size:12px;color:var(--muted);margin-top:2px}.small{font-size:11.5px;color:var(--faint)}
.status{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:4px 8px;font-size:11.5px;white-space:nowrap;background:var(--okSoft);color:var(--ok)}
.status.neutral{background:var(--paper);color:var(--muted);border:1px solid var(--line)}.status.warn{background:var(--warnSoft);color:var(--warn)}
.dot{width:6px;height:6px;border-radius:50%;background:currentColor}
.modelmark{width:36px;height:36px;border-radius:10px;background:var(--accentSoft);color:var(--accent);display:grid;place-items:center;font-weight:700;font-size:15px}
.btn{border:1px solid var(--line2);background:var(--panel);padding:7px 11px;border-radius:8px;font-size:12px;cursor:pointer;white-space:nowrap}
.btn.primary{border-color:var(--accent);color:var(--accent);font-weight:600}.btn.primary:hover{background:var(--accentSoft)}
.btn.quiet{border:0;color:var(--faint);background:transparent}.btn.danger{border:0;color:var(--danger);background:transparent}
.privacy{display:flex;gap:9px;padding:11px 14px;background:var(--okSoft);color:var(--ok);border-radius:10px;margin-bottom:13px;font-size:12.5px}
.progress{height:6px;border-radius:99px;background:var(--line);overflow:hidden;margin:8px 0 4px}.progress span{display:block;height:100%;width:59%;background:var(--accent);border-radius:99px}
.reqs{padding:13px 16px;background:var(--panel2);border-top:1px solid var(--line);display:grid;grid-template-columns:1fr 1fr;gap:10px 18px}
.req{font-size:12px;color:var(--muted)}.req b{display:block;color:var(--ink);font-size:12px;margin-bottom:2px}
.testbox{padding:14px 16px;border-top:1px solid var(--line);background:var(--panel2)}
.quote{margin-top:9px;padding:10px 12px;border:1px solid var(--line);background:var(--panel);border-radius:9px;font-size:13px}
.composer-demo{margin-top:22px}.composer-label{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--faint);margin-bottom:7px}
.composer{border:1px solid var(--line);background:var(--panel);border-radius:15px;padding:12px 13px 8px;box-shadow:0 2px 8px rgba(20,20,15,.05)}
.placeholder{color:var(--faint);font-size:13px;min-height:40px}.controls{display:flex;align-items:center;gap:7px}
.round{border:0;background:transparent;width:28px;height:28px;border-radius:7px;display:grid;place-items:center;color:var(--muted);position:relative}
.mic.locked{color:var(--faint);background:var(--paper);cursor:pointer}.mic.ready{color:var(--accent);cursor:pointer}.spacer{flex:1}.send{background:var(--paper);color:var(--faint)}
.tip{position:absolute;bottom:36px;left:50%;transform:translateX(-50%);background:var(--ink);color:var(--panel);padding:6px 8px;border-radius:7px;font-size:11px;white-space:nowrap;opacity:0;pointer-events:none;transition:.12s}
.mic.locked:hover .tip,.mic.locked:focus .tip{opacity:1}.note{font-size:11.5px;color:var(--faint);margin-top:7px}
.note a{color:var(--accent);text-decoration:none;margin-left:5px}.note a:hover{text-decoration:underline}
@media(max-width:760px){body{padding:8px}.stage{grid-template-columns:1fr}.nav{display:none}main{padding:20px 14px}.reqs{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="caption"><b>§37 — Voice input is prepared before it appears ready.</b> Use the state switcher to review the complete setup contract. The mock follows the current Settings shell and composer control language; it is self-contained, supports dark mode, and fetches no assets.</div>
<div class="stage">
<nav class="nav">
<div class="brand"><span></span> Settings</div>
<button><span class="ico"></span>Appearance</button>
<button><span class="ico"></span>Files</button>
<button><span class="ico"></span>Models</button>
<button class="active"><span class="ico"></span>Voice input</button>
<button><span class="ico"></span>Personas</button>
</nav>
<main><div class="main-inner">
<h1>Voice input</h1>
<div class="sub">Speak naturally in the composer. Your recording is transcribed on this device and is never sent to a speech service.</div>
<div class="states" role="tablist" aria-label="Mock states">
<button class="active" data-state="setup">Not set up</button>
<button data-state="downloading">Downloading</button>
<button data-state="ready">Ready</button>
</div>
<section class="screen active" data-screen="setup">
<div class="privacy"><span></span><span><b>Private by design.</b> Audio and transcripts stay on this computer.</span></div>
<div class="card">
<div class="row"><div class="row-main"><div class="title">This Mac is supported</div><div class="help">macOS 14.6 · Apple M1 · 16 GB memory</div></div><span class="status"><span class="dot"></span>Compatible</span></div>
<div class="reqs">
<div class="req"><b>Mac</b>macOS 12+ · Apple Silicon (M1 or newer)</div>
<div class="req"><b>Windows</b>Windows 10 22H2 or 11 · x64 processor</div>
<div class="req"><b>Memory</b>8 GB recommended</div>
<div class="req"><b>Processor</b>4 CPU cores recommended</div>
</div>
</div>
<div class="card">
<div class="row"><span class="modelmark">W</span><div class="row-main"><div class="title">Whisper Base · English</div><div class="help">Fast local transcription for short prompts</div><div class="small">Download size 142 MiB · installed only for Voice input</div></div><button class="btn primary">Download model</button></div>
</div>
<div class="card"><div class="row"><div class="row-main"><div class="title">Microphone test</div><div class="help">Available after the model is downloaded and verified.</div></div><span class="status neutral">Waiting for model</span></div></div>
</section>
<section class="screen" data-screen="downloading">
<div class="card">
<div class="row"><span class="modelmark">W</span><div class="row-main"><div class="title">Downloading Whisper Base · English</div><div class="progress"><span></span></div><div class="small">84 MiB of 142 MiB · about 18 seconds remaining</div></div><button class="btn quiet">Cancel</button></div>
<div class="row"><div class="row-main"><div class="title">What happens next</div><div class="help">OpenCoworker verifies the model, then asks you to test the microphone. You can leave Settings while this downloads.</div></div><span class="status warn"><span class="dot"></span>Not ready</span></div>
</div>
<div class="card"><div class="row"><div class="row-main"><div class="title">Your device remains compatible</div><div class="help">macOS 14.6 · Apple M1 · 16 GB memory</div></div><span class="status"><span class="dot"></span>Compatible</span></div></div>
</section>
<section class="screen" data-screen="ready">
<div class="privacy"><span></span><span><b>Voice input is ready.</b> Transcription runs locally on this computer.</span></div>
<div class="card">
<div class="row"><span class="modelmark">W</span><div class="row-main"><div class="title">Whisper Base · English</div><div class="help">Installed and verified · 142 MiB</div></div><span class="status"><span class="dot"></span>Ready</span><button class="btn danger">Delete model</button></div>
<div class="testbox"><div class="title">Microphone test passed</div><div class="help">Your microphone and local transcription engine are working.</div><div class="quote">“This is a test of voice input in OpenCoworker.”</div><button class="btn" style="margin-top:10px">Test again</button></div>
</div>
<div class="card"><div class="row"><div class="row-main"><div class="title">This Mac</div><div class="help">macOS 14.6 · Apple M1 · 16 GB memory</div></div><span class="status"><span class="dot"></span>Compatible</span></div></div>
</section>
<div class="composer-demo">
<div class="composer-label">Composer behavior in this state</div>
<div class="composer">
<div class="placeholder">Ask OpenCoworker anything…</div>
<div class="controls"><button class="round"></button><button class="round"></button><button class="round mic locked" aria-disabled="true"><span class="tip">Configure Voice Input in Settings</span></button><span class="small mic-copy">Voice input needs setup</span><span class="spacer"></span><button class="round send"></button></div>
</div>
<div class="note">Before Ready, the muted mic remains focusable and opens this Settings section. After Ready it becomes the normal record/stop control. <a href="voice-input-composer-states.html">Open the four composer states →</a></div>
</div>
</div></main>
</div>
<script>
const stateButtons=[...document.querySelectorAll('[data-state]')];
const screens=[...document.querySelectorAll('[data-screen]')];
const mic=document.querySelector('.mic');
const micCopy=document.querySelector('.mic-copy');
stateButtons.forEach(button=>button.addEventListener('click',()=>{
const state=button.dataset.state;
stateButtons.forEach(item=>item.classList.toggle('active',item===button));
screens.forEach(screen=>screen.classList.toggle('active',screen.dataset.screen===state));
const ready=state==='ready';
mic.classList.toggle('locked',!ready);mic.classList.toggle('ready',ready);
mic.setAttribute('aria-disabled',String(!ready));
micCopy.textContent=ready?'Voice input ready':state==='downloading'?'Voice model is downloading':'Voice input needs setup';
}));
</script>
</body>
</html>