fenix-claude-plugin 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.claude-plugin/plugin.json +13 -13
  2. package/.mcp.json +11 -11
  3. package/README.md +33 -40
  4. package/artifacts/collections.html +149 -0
  5. package/artifacts/fenix-workspace.html +1285 -0
  6. package/artifacts/oa-editor.html +210 -0
  7. package/artifacts/patent-editor.html +325 -0
  8. package/artifacts/review-workspace.html +1135 -0
  9. package/commands/fenix-install.md +19 -11
  10. package/commands/fenix-setup.md +18 -13
  11. package/package.json +16 -16
  12. package/skills/fenix-application/SKILL.md +35 -12
  13. package/skills/fenix-application/VERSION +1 -1
  14. package/skills/fenix-application/references/claim1-method-drafting.md +1 -1
  15. package/skills/fenix-application/references/claims.md +4 -5
  16. package/skills/fenix-application/references/disclosure.md +116 -10
  17. package/skills/fenix-application/references/editors.md +222 -51
  18. package/skills/fenix-application/references/figure-description.md +1 -1
  19. package/skills/fenix-application/references/figure-strategy.md +166 -0
  20. package/skills/fenix-application/references/figures-guidance.md +1 -1
  21. package/skills/fenix-application/references/figures.md +51 -14
  22. package/skills/fenix-application/references/patent-review-checklist.md +3 -1
  23. package/skills/fenix-application/references/patent-review.md +6 -2
  24. package/skills/fenix-application/references/patent-safe.md +1 -1
  25. package/skills/fenix-application/references/patent.md +9 -9
  26. package/skills/fenix-application/references/spec-guidance.md +1 -1
  27. package/skills/fenix-application/references/spec.md +17 -7
  28. package/skills/fenix-application/scripts/figure_strategy_server.py +136 -0
  29. package/skills/fenix-diagram/SKILL.md +54 -0
  30. package/skills/fenix-diagram/VERSION +1 -0
  31. package/skills/fenix-diagram/references/diagram-svg-guide.md +271 -0
  32. package/skills/fenix-office-action/SKILL.md +6 -4
  33. package/skills/fenix-office-action/VERSION +1 -1
  34. package/skills/fenix-office-action/references/oa-analysis.md +14 -4
  35. package/skills/fenix-office-action/references/oa-drafting.md +98 -21
  36. package/skills/fenix-office-action/references/oa-response.md +13 -13
  37. package/skills/fenix-project/SKILL.md +4 -3
  38. package/skills/fenix-project/VERSION +1 -1
  39. package/skills/fenix-project/references/create-project.md +11 -1
  40. package/artifacts/claims-editor.html +0 -205
  41. package/artifacts/figures-editor.html +0 -237
  42. package/artifacts/oa-arguments-editor.html +0 -216
  43. package/artifacts/practice-dashboard.html +0 -469
  44. package/artifacts/project-overview.html +0 -481
  45. package/artifacts/sections-editor.html +0 -205
  46. package/artifacts/user-dashboard.html +0 -368
  47. package/skills/fenix-application/CUSTOMIZE.md +0 -1
  48. package/skills/fenix-application/assets/claims_editor.html +0 -86
  49. package/skills/fenix-application/assets/figures_editor.html +0 -86
  50. package/skills/fenix-application/assets/spec_editor.html +0 -86
  51. package/skills/fenix-office-action/CUSTOMIZE.md +0 -1
  52. package/skills/fenix-project/CUSTOMIZE.md +0 -1
@@ -1,205 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>Spec Sections Editor</title>
6
- <style>
7
- *{box-sizing:border-box;margin:0;padding:0}
8
- body{font-family:system-ui,-apple-system,sans-serif;background:#fff7ed;min-height:100vh;padding:20px 16px}
9
- .card{background:#fff;max-width:900px;margin:0 auto;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.12);overflow:hidden}
10
- .hdr{display:flex;align-items:center;gap:12px;padding:14px 20px;border-bottom:1px solid #fed7aa}
11
- .hdr h1{font-size:15px;font-weight:600;color:#1e293b}.hdr .rev{font-size:12px;color:#94a3b8}
12
- .hdr .toggle-all{margin-left:auto;flex-shrink:0}
13
- .bdy{padding:20px}.sub{font-size:13px;color:#64748b;margin-bottom:14px}
14
- input,select,textarea{font-family:inherit;font-size:13px;border:1.5px solid #fed7aa;border-radius:5px;padding:6px 10px;outline:none;width:100%;transition:border-color .15s;background:#fff}
15
- input:focus,select:focus,textarea:focus{border-color:#ea580c}
16
- .field{margin-bottom:12px}label{display:block;font-size:12px;font-weight:600;color:#374151;margin-bottom:4px}
17
- .acc{border:1px solid #fed7aa;overflow:hidden;margin-bottom:-1px}
18
- .acc-hd{display:flex;align-items:center;gap:10px;padding:11px 14px;cursor:pointer;background:#fffbf5;user-select:none}
19
- .acc-hd:hover{background:#fff7ed}
20
- .acc-hd .chev{color:#ea580c;font-size:12px;transition:transform .15s;flex-shrink:0}
21
- .acc.open .chev{transform:rotate(90deg)}
22
- .acc-hd .ttl{font-weight:600;font-size:13px;color:#1e293b;flex-shrink:0}
23
- .acc-hd .prev{font-size:12px;color:#94a3b8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
24
- .acc-bd{display:none;padding:14px;border-top:1px solid #fed7aa;background:#fff}
25
- .acc.open .acc-bd{display:block}
26
- .btn-sm{padding:4px 12px;background:#fff;border:1.5px solid #fed7aa;border-radius:5px;font-size:12px;cursor:pointer;color:#374151}
27
- .btn-sm:hover{border-color:#ea580c}
28
- .rm-item{margin-left:auto;color:#dc2626;background:none;border:none;cursor:pointer;font-size:13px;flex-shrink:0}
29
- .bar{margin-top:16px;display:flex;align-items:center;gap:14px}
30
- .cta{padding:9px 24px;background:#ea580c;color:#fff;border:none;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer;transition:background .15s}
31
- .cta:hover{background:#c2410c}.cta:disabled{opacity:.5;cursor:default}
32
- #st{font-size:13px;font-weight:500}#st.ok{color:#16a34a}#st.err{color:#dc2626}#st.busy{color:#64748b}
33
- textarea.sec{min-height:200px;font-family:Menlo,Consolas,monospace;line-height:1.6;resize:vertical}
34
- .addbar{margin:6px 0 4px}
35
- .diff{display:none;margin-top:16px;border:1px solid #fed7aa;border-radius:6px;overflow:hidden}
36
- .diff.show{display:block}
37
- .diff-hd{display:flex;align-items:center;gap:10px;padding:8px 12px;background:#fff7ed;font-size:12px;font-weight:600;color:#c2410c}
38
- .diff pre{margin:0;padding:12px;font-size:12px;font-family:Menlo,Consolas,monospace;line-height:1.5;white-space:pre-wrap;max-height:240px;overflow:auto;color:#1e293b}
39
- .ftr{padding:9px 20px;border-top:1px solid #fed7aa;font-size:11px;color:#94a3b8;display:flex;justify-content:space-between}
40
- </style>
41
- </head>
42
- <body>
43
- <div class="card">
44
- <div class="hdr"><h1>Spec Sections Editor</h1><span class="rev" id="revLbl"></span><button class="btn-sm toggle-all" id="toggleAll" onclick="toggleAll()" style="display:none">Expand all</button></div>
45
- <div class="bdy">
46
- <p class="sub" id="sub">Loading current spec sections from Fenix…</p>
47
- <div id="acc"></div>
48
- <div class="addbar"><button class="btn-sm" onclick="addItem()">+ Add section</button></div>
49
- <div class="bar"><button class="cta" id="saveBtn" onclick="save()" disabled>Save to Fenix</button><span id="st"></span></div>
50
- <div class="diff" id="diffPanel">
51
- <div class="diff-hd"><span>Saved. Paste this summary into chat so the assistant stays in sync:</span><button class="btn-sm" style="margin-left:auto" onclick="copyDiff()">Copy summary</button></div>
52
- <pre id="diffText"></pre>
53
- </div>
54
- </div>
55
- <div class="ftr"><span>Fenix Patent Assistant — live editor</span><span>v1</span></div>
56
- </div>
57
- <script>
58
- // The assistant replaces __PROJECT_ID__ with the real Fenix project id before opening this artifact.
59
- var PROJECT_ID = "__PROJECT_ID__";
60
- // The assistant also substitutes this connector's MCP tool prefix here, e.g. "mcp__<connector-id>__"
61
- // (the prefix differs per connection, so it cannot be hardcoded). If left unsubstituted, call()
62
- // falls back to the bare tool name (which works on hosts that don't namespace MCP tools).
63
- var SERVER_PREFIX = "__SERVER_PREFIX__";
64
-
65
- var baseline = {}; // { name: plainText } last loaded from / saved to Fenix
66
- var baselineRev = null;
67
-
68
- function unwrap(res){
69
- if(res==null) return res;
70
- if(typeof res==="string"){ try{ return JSON.parse(res); }catch(e){ return res; } }
71
- if(Array.isArray(res)) return res;
72
- if(typeof res==="object" && Array.isArray(res.content)){
73
- var t=res.content.map(function(c){ return (c&&c.text)||""; }).join("");
74
- try{ return JSON.parse(t); }catch(e){ return res; }
75
- }
76
- if(res && res.result!==undefined) return unwrap(res.result);
77
- return res;
78
- }
79
- function toolName(n){ return (SERVER_PREFIX && SERVER_PREFIX.indexOf("__SERVER")!==0) ? SERVER_PREFIX + n : n; }
80
- async function call(name,args){
81
- var tries=[toolName(name)]; if(tries[0]!==name) tries.push(name);
82
- var err; for(var i=0;i<tries.length;i++){ try{ return unwrap(await window.cowork.callMcpTool(tries[i],args||{})); }catch(e){ err=e; } }
83
- throw err;
84
- }
85
-
86
- function elt(tag,cls,txt){ var e=document.createElement(tag); if(cls)e.className=cls; if(txt!=null)e.textContent=txt; return e; }
87
- function makeAcc(headerTitle,preview,bodyBuilder){
88
- var wrap=elt("div","acc"); var hd=elt("div","acc-hd");
89
- hd.appendChild(elt("span","chev","▶")); hd.appendChild(elt("span","ttl",headerTitle));
90
- var prev=elt("span","prev",preview||""); hd.appendChild(prev);
91
- var rm=elt("button","rm-item","Remove"); rm.onclick=function(ev){ ev.stopPropagation(); wrap.remove(); syncToggle(); }; hd.appendChild(rm);
92
- hd.onclick=function(){ wrap.classList.toggle("open"); syncToggle(); };
93
- var bd=elt("div","acc-bd"); bodyBuilder(bd,prev);
94
- wrap.appendChild(hd); wrap.appendChild(bd); return wrap;
95
- }
96
- function secBody(bd,prev,name,text){
97
- var f1=elt("div","field"); f1.appendChild(elt("label",null,"Section Name")); var ni=document.createElement("input"); ni.className="s-name"; ni.value=name||""; f1.appendChild(ni); bd.appendChild(f1);
98
- var f2=elt("div","field"); f2.appendChild(elt("label",null,"Text (blank line = new paragraph)")); var ta=document.createElement("textarea"); ta.className="s-txt sec"; ta.value=text||""; ta.oninput=function(){ prev.textContent=(ta.value||"").replace(/\n/g," ").slice(0,80); }; f2.appendChild(ta); bd.appendChild(f2);
99
- }
100
- function addSection(name,text,orig){ var acc=document.getElementById("acc"); var w=makeAcc(name||"Section",(text||"").replace(/\n/g," ").slice(0,80),function(bd,prev){ secBody(bd,prev,name,text); }); w.__sec=orig||{}; acc.appendChild(w); return w; }
101
- function addItem(){ addSection("New Section","").classList.add("open"); syncToggle(); }
102
- function syncToggle(){ var btn=document.getElementById("toggleAll"); var accs=document.querySelectorAll("#acc .acc"); if(!accs.length){ btn.style.display="none"; return; } btn.style.display=""; var anyClosed=Array.from(accs).some(function(a){ return !a.classList.contains("open"); }); btn.textContent=anyClosed?"Expand all":"Collapse all"; }
103
- function toggleAll(){ var accs=document.querySelectorAll("#acc .acc"); var anyClosed=Array.from(accs).some(function(a){ return !a.classList.contains("open"); }); accs.forEach(function(a){ if(anyClosed){ a.classList.add("open"); } else { a.classList.remove("open"); } }); syncToggle(); }
104
-
105
- // Strip <p> wrappers to editable plain text (blank line between paragraphs).
106
- function paragraphsToText(paras){ return (paras||[]).map(function(p){ return String(p).replace(/<[^>]+>/g,"").trim(); }).filter(Boolean).join("\n\n"); }
107
- // Editable text -> Fenix paragraph array (each paragraph wrapped in <p>).
108
- function textToParagraphs(tx){ return String(tx||"").split(/\n\n+/).map(function(p){ return p.trim(); }).filter(Boolean).map(function(p){ return "<p>"+p.replace(/\n/g," ")+"</p>"; }); }
109
-
110
- function collect(){
111
- var secs={}; document.querySelectorAll("#acc .acc").forEach(function(a){
112
- var nm=a.querySelector(".s-name").value.trim(); var tx=a.querySelector(".s-txt").value;
113
- // Overlay edits onto the ORIGINAL section so any fields besides paragraphs survive.
114
- if(nm){ secs[nm]=Object.assign({}, a.__sec||{}, { paragraphs:textToParagraphs(tx) }); }
115
- }); return secs;
116
- }
117
-
118
- // ---- Diff by section name (compare normalized text) ----
119
- function normText(tx){ return String(tx||"").replace(/\s+/g," ").trim(); }
120
- function diff(oldMap,newSecs){
121
- // newSecs is { name: { paragraphs } }; oldMap is { name: plainText }
122
- var newMap={}; Object.keys(newSecs).forEach(function(k){ newMap[k]=paragraphsToText(newSecs[k].paragraphs); });
123
- var changed=[], added=[], removed=[], unchanged=[];
124
- Object.keys(newMap).forEach(function(k){
125
- if(!(k in oldMap)){ added.push({name:k,text:newMap[k]}); }
126
- else if(normText(newMap[k])!==normText(oldMap[k])){ changed.push({name:k,text:newMap[k]}); }
127
- else { unchanged.push(k); }
128
- });
129
- Object.keys(oldMap).forEach(function(k){ if(!(k in newMap)) removed.push(k); });
130
- changed.sort(function(a,b){ return a.name<b.name?-1:1; }); added.sort(function(a,b){ return a.name<b.name?-1:1; });
131
- removed.sort(); unchanged.sort();
132
- return { changed:changed, added:added, removed:removed, unchanged:unchanged };
133
- }
134
- function summaryText(d,rev){
135
- var lines=["[SECTIONS SAVED - sectionsRev "+(rev==null?"?":rev)+"]"];
136
- if(!d.changed.length && !d.added.length && !d.removed.length){ lines.push("No section changes (re-saved identical set)."); return lines.join("\n"); }
137
- if(d.changed.length){ lines.push(""); lines.push("Changed sections (full text):"); d.changed.forEach(function(s){ lines.push("## "+s.name); lines.push(s.text); }); }
138
- if(d.added.length){ lines.push(""); lines.push("Added sections (full text):"); d.added.forEach(function(s){ lines.push("## "+s.name); lines.push(s.text); }); }
139
- if(d.removed.length){ lines.push(""); lines.push("Removed sections: "+d.removed.join(", ")); }
140
- if(d.unchanged.length){ lines.push(""); lines.push("Unchanged: "+d.unchanged.join(", ")); }
141
- return lines.join("\n");
142
- }
143
-
144
- function setStatus(msg,cls){ var s=document.getElementById("st"); s.textContent=msg||""; s.className=cls||""; }
145
-
146
- async function load(){
147
- if(!(window.cowork && typeof window.cowork.callMcpTool==="function")){
148
- document.getElementById("sub").textContent="";
149
- setStatus("⚠ Open this as a LIVE ARTIFACT, not a file preview — it needs the Cowork bridge (window.cowork) to read and save Fenix data. Ask the assistant to open it as a live artifact.","err");
150
- return;
151
- }
152
- try{
153
- var data=await call("get_project",{ projectId:PROJECT_ID, include:["sections"] });
154
- var sections=(data&&data.sections)||{};
155
- baseline={}; document.getElementById("acc").innerHTML="";
156
- Object.keys(sections).forEach(function(name){
157
- var txt=paragraphsToText(sections[name]&&sections[name].paragraphs);
158
- baseline[name]=txt; addSection(name,txt,sections[name]);
159
- });
160
- baselineRev=(data&&data.revs&&typeof data.revs.sections!=="undefined")?data.revs.sections:null;
161
- document.getElementById("sub").textContent="Open any section to edit it. Blank line = new paragraph. Add or remove sections, then Save to Fenix.";
162
- document.getElementById("revLbl").textContent=baselineRev==null?"":("rev "+baselineRev);
163
- document.getElementById("saveBtn").disabled=false;
164
- syncToggle();
165
- }catch(e){
166
- document.getElementById("sub").textContent="";
167
- setStatus("Could not load sections: "+((e&&e.message)||e),"err");
168
- }
169
- }
170
-
171
- async function save(){
172
- var btn=document.getElementById("saveBtn"); btn.disabled=true; setStatus("Saving…","busy");
173
- document.getElementById("diffPanel").classList.remove("show");
174
- var sections=collect();
175
- try{
176
- var res=await call("save_project",Object.assign({ projectId:PROJECT_ID, type:"sections", sections:sections }, (baselineRev!=null?{baseRev:baselineRev}:{})));
177
- if(res && res.conflict){ setStatus("⚠ Not saved — the sections changed in Fenix since you opened this editor (now at rev "+res.currentRev+"). Reload to get the latest, then re-apply your edits.","err"); return; }
178
- if(res && res.isError){ throw new Error(res.error||"save failed"); }
179
- var rev=(res&&typeof res.sectionsRev!=="undefined")?res.sectionsRev:null;
180
- var d=diff(baseline,sections);
181
- var summary=summaryText(d,rev);
182
- // Advance baseline to the saved set (as plain text).
183
- baseline={}; Object.keys(sections).forEach(function(k){ baseline[k]=paragraphsToText(sections[k].paragraphs); }); baselineRev=rev;
184
- document.getElementById("revLbl").textContent=rev==null?"":("rev "+rev);
185
- var nChanged=d.changed.length+d.added.length+d.removed.length;
186
- setStatus("✓ Saved to Fenix"+(rev==null?"":" (rev "+rev+")")+(nChanged?" — "+nChanged+" section(s) changed":""),"ok");
187
- document.getElementById("diffText").textContent=summary;
188
- document.getElementById("diffPanel").classList.add("show");
189
- document.getElementById("diffPanel").__summary=summary;
190
- try{ if(window.cowork && typeof window.cowork.sendPrompt==="function"){ window.cowork.sendPrompt(summary); } }catch(e){}
191
- }catch(e){ setStatus("Save failed: "+((e&&e.message)||e),"err"); }
192
- finally{ btn.disabled=false; }
193
- }
194
-
195
- function copyDiff(){
196
- var txt=document.getElementById("diffPanel").__summary||document.getElementById("diffText").textContent;
197
- var done=function(){ setStatus("Summary copied — paste it into chat.","ok"); };
198
- if(navigator.clipboard){ navigator.clipboard.writeText(txt).then(done).catch(fb); } else { fb(); }
199
- function fb(){ var el=document.createElement("textarea"); el.value=txt; el.style.cssText="position:fixed;top:0;left:0;opacity:0;"; document.body.appendChild(el); el.select(); try{ document.execCommand("copy"); done(); }catch(e){} document.body.removeChild(el); }
200
- }
201
-
202
- window.onload=load;
203
- </script>
204
- </body>
205
- </html>
@@ -1,368 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Team Member</title>
7
- <style>
8
- :root{
9
- color-scheme: dark;
10
- --bg:#0c0e13; --card:#14171f; --card2:#191d27; --ink:#c3c8d4; --muted:#7e8598;
11
- --line:#242936; --line2:#2f3543;
12
- --accent:#818cf8; --accent2:#6366f1; --accentbg:#1c2030; --accentglow:#2a2f4a;
13
- --red:#f87171; --redbg:#2a1417; --orange:#fb923c; --orangebg:#2a1c10;
14
- --amber:#fbbf24; --amberbg:#2a230f; --green:#34d399; --greenbg:#0f2a20;
15
- --gray:#8b92a6; --graybg:#1b1f29; --purple:#a78bfa; --purplebg:#211a33;
16
- }
17
- *{box-sizing:border-box}
18
- body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
19
- .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
20
- .wrap{max-width:1280px;margin:0 auto;padding:22px 20px 80px}
21
- .top{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-bottom:4px}
22
- .brand{display:flex;align-items:center;gap:13px}
23
- .brand .logo{display:inline-flex;align-items:center;justify-content:center;width:46px;height:46px;border-radius:11px;border:1px solid var(--line2);background:#0a0c11;line-height:0}
24
- .brand .logo img{display:block;width:38px;height:38px;object-fit:contain}
25
- h1{font-size:22px;margin:0;letter-spacing:-.02em;font-weight:800}
26
- .sub{color:var(--muted);font-size:12.5px;margin-top:3px}
27
- .prog{display:flex;flex-wrap:wrap;gap:7px;margin:14px 0 16px;min-height:24px}
28
- .chip{display:flex;align-items:center;gap:7px;background:var(--card);border:1px solid var(--line);border-radius:20px;padding:4px 11px;font-size:11.5px;color:var(--muted)}
29
- .chip.done{color:var(--green);border-color:transparent;background:var(--greenbg)}
30
- .chip.err{color:var(--red);border-color:transparent;background:var(--redbg)}
31
- .chip .sp{width:11px;height:11px;border:2px solid var(--line2);border-top-color:var(--accent);border-radius:50%;animation:s .7s linear infinite;display:inline-block}
32
- .bar{display:flex;flex-wrap:wrap;gap:10px;align-items:flex-end;margin-bottom:18px}
33
- .fld{display:flex;flex-direction:column;gap:5px}
34
- .fld label{font-size:10.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:700}
35
- select{background:var(--card2);border:1px solid var(--line2);border-radius:9px;padding:9px 12px;color:var(--ink);font-size:13px;min-width:300px}
36
- .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:11px;margin-bottom:20px}
37
- .kpi{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:15px 16px;position:relative;overflow:hidden}
38
- .kpi .n{font-size:30px;font-weight:800;letter-spacing:-.03em;line-height:1}
39
- .kpi .l{color:var(--muted);font-size:11.5px;margin-top:7px}
40
- .kpi.accent .n{color:var(--accent)}
41
- .kpi.purple{background:linear-gradient(180deg,var(--purplebg),var(--card));border-color:#2e2547}
42
- .kpi.purple .n{color:var(--purple)}
43
- .kpi.red{background:linear-gradient(180deg,var(--redbg),var(--card));border-color:#3a1f24}
44
- .kpi.red .n{color:var(--red)}
45
- .ld{color:var(--muted);opacity:.4}
46
- .panel{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:18px}
47
- .panel h3{margin:0 0 14px;font-size:11.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:700;display:flex;justify-content:space-between;align-items:center;gap:8px}
48
- .panel h3 .cnt{color:var(--ink);font-weight:800}
49
- .profile{display:flex;align-items:center;gap:15px;margin-bottom:6px}
50
- .av{width:54px;height:54px;border-radius:50%;background:var(--accentbg);color:var(--accent);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:17px;flex-shrink:0;border:1px solid var(--line2)}
51
- .pname{font-size:18px;font-weight:800;margin:0;letter-spacing:-.01em}
52
- .pmeta{color:var(--muted);font-size:12.5px;margin-top:3px}
53
- .pill{display:inline-block;padding:2px 10px;border-radius:20px;font-size:11px;font-weight:700;white-space:nowrap;margin-left:8px;vertical-align:middle}
54
- .due{font-weight:700;white-space:nowrap}
55
- .days{display:inline-block;margin-left:7px;padding:1px 8px;border-radius:6px;font-size:10.5px;font-weight:700}
56
- .lk{color:var(--accent);text-decoration:none;font-weight:700;font-size:12px;white-space:nowrap}
57
- .lk:hover{text-decoration:underline}
58
- .center{text-align:center;color:var(--muted);padding:30px 0}
59
- .spin{width:26px;height:26px;border:3px solid var(--line2);border-top-color:var(--accent);border-radius:50%;animation:s .7s linear infinite;margin:0 auto 13px}
60
- @keyframes s{to{transform:rotate(360deg)}}
61
- .atable{width:100%;border-collapse:separate;border-spacing:0}
62
- .atable th{text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);font-weight:700;padding:0 8px 9px}
63
- .atable td{padding:11px 8px;border-top:1px solid var(--line);vertical-align:middle}
64
- .title{font-weight:700}
65
- .meta{color:var(--muted);font-size:12px;margin-top:2px}
66
- .dot{width:9px;height:9px;border-radius:50%;display:inline-block;margin-right:8px;vertical-align:middle}
67
- .hbar{height:7px;border-radius:5px;background:var(--graybg);overflow:hidden;margin:4px 0 6px}
68
- .hbar > i{display:block;height:100%;background:linear-gradient(90deg,var(--accent2),var(--accent));border-radius:5px}
69
- .num{font-weight:800}
70
- .cbtn{background:var(--card2);border:1px solid var(--line2);color:var(--ink);border-radius:8px;padding:5px 11px;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap;font-family:inherit}
71
- .cbtn:hover{background:var(--greenbg);border-color:transparent;color:var(--green)}
72
- .cbtn.arm{background:var(--green);border-color:transparent;color:#06281d}
73
- .cbtn:disabled{opacity:.55;cursor:default}
74
- .hint{background:var(--card2);border:1px solid var(--line);border-radius:11px;padding:11px 14px;color:var(--muted);font-size:12px;margin-top:6px}
75
- @media(max-width:980px){ .kpis{grid-template-columns:repeat(2,1fr)} }
76
- @media(max-width:560px){ select{min-width:0;width:100%} }
77
- </style>
78
- </head>
79
- <body>
80
- <div class="wrap">
81
- <div class="top">
82
- <div class="brand">
83
- <span class="logo" aria-label="Fenix logo" role="img"><img src="data:image/png;base64,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" alt="Fenix logo" width="38" height="38"></span>
84
- <div>
85
- <h1>Team Member</h1>
86
- <div class="sub" id="sub">Live · loading…</div>
87
- </div>
88
- </div>
89
- </div>
90
-
91
- <div class="bar" style="align-items:flex-end">
92
- <div class="fld">
93
- <label for="userSel">Team member</label>
94
- <select id="userSel"><option>Loading…</option></select>
95
- </div>
96
- <a href="#" id="resetDef" class="lk" style="display:none;padding-bottom:10px">↺ Reset to default</a>
97
- <div class="prog" id="prog" style="margin:0 0 4px auto"></div>
98
- </div>
99
-
100
- <div class="kpis" id="kpis"></div>
101
-
102
- <div class="panel" id="profilePanel" style="margin-bottom:14px">
103
- <h3>Profile</h3>
104
- <div class="center"><div class="spin"></div>Loading team…</div>
105
- </div>
106
-
107
- <div class="panel">
108
- <h3>Active tasks <span class="cnt" id="taskCount"></span></h3>
109
- <div id="tasksBody"><div class="center">Select a team member to see their open tasks.</div></div>
110
- </div>
111
- </div>
112
-
113
- <!--
114
- ┌─────────────────────────────────────────────────────────────────────────┐
115
- │ CLAUDE: SET THE DEFAULT TEAM MEMBER HERE │
116
- │ │
117
- │ When the user asks to open this view "for <person>", edit the │
118
- │ DEFAULT_USER constant in the <script> below to that person's INITIALS │
119
- │ (e.g. "MAC") or full NAME (e.g. "Michael Carey"), then re-publish the │
120
- │ artifact with update_artifact. │
121
- │ │
122
- │ Behavior: │
123
- │ • FIRST load (no saved choice yet) → the view selects DEFAULT_USER. │
124
- │ • After the user picks anyone, that choice is saved to localStorage │
125
- │ ("fx_user") and OVERRIDES DEFAULT_USER on every later open. │
126
- │ • To force the default to win again, the user clears it — there is a │
127
- │ "Reset to default" link next to the dropdown that wipes fx_user. │
128
- │ • Leave DEFAULT_USER = "" to just open on whoever sorts first. │
129
- │ │
130
- │ Valid initials come from list_users (e.g. NJZ, MAC, BJA, STA, ...). │
131
- └─────────────────────────────────────────────────────────────────────────┘
132
- -->
133
- <script>
134
- const SRV='mcp__25de2133-8f0a-47c4-b66d-b520d6be5fe7__';
135
- const BASE='https://chau.fenix.ai/dashboard/projects/';
136
-
137
- // ▼▼▼ CLAUDE: edit this one value to choose who the view opens on first load. ▼▼▼
138
- const DEFAULT_USER = ""; // initials ("MAC") or full name ("Michael Carey"); "" = first in list
139
- // ▲▲▲ Saved user selections (localStorage "fx_user") override this after first use. ▲▲▲
140
- let USERS=[],PROJECTS=[],TASKS=[];
141
- const today=(()=>{const n=new Date();return new Date(n.getFullYear(),n.getMonth(),n.getDate())})();
142
-
143
- function unwrap(res){
144
- if(res==null) return null;
145
- if(typeof res==='string'){ try{return JSON.parse(res)}catch(e){return res} }
146
- if(Array.isArray(res)) return res;
147
- if(Array.isArray(res.content)){ const t=res.content.map(c=>c&&c.text||'').join(''); try{return JSON.parse(t)}catch(e){return res} }
148
- if(res.result!==undefined) return unwrap(res.result);
149
- return res;
150
- }
151
- function asArray(res,key){ const u=unwrap(res); if(Array.isArray(u))return u; if(u&&Array.isArray(u[key]))return u[key]; return []; }
152
- function call(tool,args){ return window.cowork.callMcpTool(SRV+tool,args||{}).catch(()=>null); }
153
- function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]))}
154
-
155
- function pd(raw){
156
- if(!raw) return null;
157
- let s=String(raw).trim(); if(!s) return null;
158
- s=s.replace(/(\d+)(st|nd|rd|th)/gi,'$1');
159
- let iso=s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
160
- if(iso) return new Date(+iso[1],+iso[2]-1,+iso[3]);
161
- let mdy=s.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})$/);
162
- if(mdy){ let mo=+mdy[1],d=+mdy[2],y=+mdy[3]; if(y<100)y+=2000; return new Date(y,mo-1,d); }
163
- let t=Date.parse(s); if(isNaN(t)) return null;
164
- const dt=new Date(t); return new Date(dt.getFullYear(),dt.getMonth(),dt.getDate());
165
- }
166
- function dleft(d){ return d?Math.round((d-today)/86400000):null; }
167
- function fmt(d){ return d?d.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}):'—'; }
168
- function urg(dl){
169
- if(dl===null) return {bg:'var(--graybg)',fg:'var(--gray)',lab:'No date'};
170
- if(dl<0) return {bg:'var(--redbg)',fg:'var(--red)',lab:Math.abs(dl)+'d overdue'};
171
- if(dl<=7) return {bg:'var(--redbg)',fg:'var(--red)',lab:dl+'d left'};
172
- if(dl<=21) return {bg:'var(--orangebg)',fg:'var(--orange)',lab:dl+'d left'};
173
- if(dl<=45) return {bg:'var(--amberbg)',fg:'var(--amber)',lab:dl+'d left'};
174
- return {bg:'var(--greenbg)',fg:'var(--green)',lab:dl+'d left'};
175
- }
176
- function dueCell(d){ const dl=d?dleft(d):null,u=urg(dl);
177
- return `<span class="due mono">${fmt(d)}</span><span class="days" style="background:${u.bg};color:${u.fg}">${u.lab}</span>`; }
178
- function initialsOf(u){ if(u.initials) return u.initials.toUpperCase();
179
- const p=(u.name||'?').split(/\s+/); return ((p[0]||'')[0]||'')+((p[1]||'')[0]||''); }
180
-
181
- function setChip(id,state,label){
182
- const el=document.getElementById('chip-'+id); if(!el) return;
183
- el.className='chip'+(state==='done'?' done':state==='err'?' err':'');
184
- el.innerHTML=(state==='load'?'<span class="sp"></span>':state==='done'?'✓ ':'✕ ')+label;
185
- }
186
- function statusDot(s){ const l=(s||'').toLowerCase();
187
- let c='var(--accent)'; if(l==='completed')c='var(--green)'; else if(l==='in-progress')c='var(--amber)'; else if(l==='inactive')c='var(--gray)';
188
- return `<span class="dot" style="background:${c}"></span>`; }
189
-
190
- // TASKS is fetched per-user (list_open_tasks({assigneeId})), so it already holds
191
- // only the selected user's tasks; just drop any that have been completed.
192
- function tasksFor(u){
193
- if(!u||!Array.isArray(TASKS)) return [];
194
- return TASKS.filter(t=>t.status!=='completed');
195
- }
196
-
197
- // Two-click confirm, then mark the task complete in Fenix via complete_task.
198
- function completeTask(taskId, btn){
199
- if(!taskId) return;
200
- if(btn.dataset.armed!=='1'){
201
- btn.dataset.armed='1'; btn.textContent='Confirm?'; btn.classList.add('arm');
202
- clearTimeout(btn._t);
203
- btn._t=setTimeout(()=>{ btn.dataset.armed='0'; btn.textContent='✓ Done'; btn.classList.remove('arm'); },4000);
204
- return;
205
- }
206
- clearTimeout(btn._t); doComplete(taskId, btn);
207
- }
208
- async function doComplete(taskId, btn){
209
- btn.disabled=true; btn.textContent='…'; btn.classList.remove('arm');
210
- const res=unwrap(await call('complete_task',{taskId}));
211
- const ok = res!=null && res!==false && !(res&&res.error);
212
- if(ok){
213
- const t=Array.isArray(TASKS)?TASKS.find(x=>x.id===taskId):null; if(t) t.status='completed';
214
- const sel=document.getElementById('userSel').value;
215
- renderKpis(USERS.find(u=>u.userId===sel)); renderTasks(sel);
216
- } else {
217
- btn.disabled=false; btn.dataset.armed='0'; btn.textContent='Retry';
218
- }
219
- }
220
-
221
- function renderKpis(u){
222
- const mine=u?tasksFor(u):[];
223
- const overdue=mine.filter(t=>t._dl!==null&&t._dl<0).length;
224
- const v=x=>u?x:'…';
225
- document.getElementById('kpis').innerHTML=`
226
- <div class="kpi accent"><div class="n mono">${v(mine.length)}</div><div class="l">Open tasks</div></div>
227
- <div class="kpi red"><div class="n mono">${v(overdue)}</div><div class="l">Overdue tasks</div></div>
228
- <div class="kpi"><div class="n mono">${u?(u.totalEstimatedHours??0):'…'}</div><div class="l">Estimated hours</div></div>
229
- <div class="kpi purple"><div class="n mono">${u?(u.totalActiveProjects??0):'…'}</div><div class="l">Active projects</div></div>`;
230
- }
231
-
232
- async function renderProfile(userId){
233
- const panel=document.getElementById('profilePanel');
234
- const u=USERS.find(x=>x.userId===userId);
235
- renderKpis(u);
236
- if(!u){ panel.innerHTML='<h3>Profile</h3><div class="center">No user selected.</div>'; return; }
237
- let avail='',email='';
238
- if(u.initials){ const info=unwrap(await call('get_user_by_initials',{initials:u.initials}));
239
- if(info&&info.found){ avail=info.availabilityStatus||''; email=info.email||''; } }
240
- const al=avail.toLowerCase();
241
- let abg='var(--graybg)',afg='var(--gray)';
242
- if(al==='busy'){abg='var(--redbg)';afg='var(--red)';}
243
- else if(al==='available'){abg='var(--greenbg)';afg='var(--green)';}
244
- const badge=avail?`<span class="pill" style="background:${abg};color:${afg}">${esc(avail)}</span>`:'';
245
- panel.innerHTML=`
246
- <h3>Profile</h3>
247
- <div class="profile">
248
- <div class="av">${esc(initialsOf(u)||'?')}</div>
249
- <div>
250
- <p class="pname">${esc(u.name||'Unknown')} ${badge}</p>
251
- <p class="pmeta">${u.initials?esc(u.initials)+' · ':''}${email?esc(email):'<span class="ld">no email on file</span>'}</p>
252
- </div>
253
- </div>
254
- <table class="atable" style="margin-top:14px">
255
- <tr><td style="border-top:0;color:var(--muted)">Estimated hours</td><td style="border-top:0;text-align:right" class="num mono">${u.totalEstimatedHours??0}</td></tr>
256
- <tr><td style="color:var(--muted)">Active projects</td><td style="text-align:right" class="num mono">${u.totalActiveProjects??0}</td></tr>
257
- </table>`;
258
- }
259
-
260
- function renderTasks(userId){
261
- const u=USERS.find(x=>x.userId===userId);
262
- const body=document.getElementById('tasksBody');
263
- const cnt=document.getElementById('taskCount');
264
- if(!u){ body.innerHTML='<div class="center">Select a team member.</div>'; cnt.textContent=''; return; }
265
- if(TASKS===null){
266
- cnt.textContent='';
267
- body.innerHTML='<div class="hint">Couldn’t load tasks for '+esc(u.name)+' (the <span class="mono">list_open_tasks</span> call failed). Estimated hours and active-project counts above still reflect this member’s load. Try reselecting the member.</div>';
268
- return;
269
- }
270
- if(TASKS.length===0 && body.dataset.loading==='1'){
271
- cnt.textContent=''; body.innerHTML='<div class="center"><div class="spin"></div>Loading tasks…</div>'; return;
272
- }
273
- const mine=tasksFor(u).slice().sort((a,b)=>(a._d?a._d:9e15)-(b._d?b._d:9e15));
274
- cnt.textContent=mine.length?'· '+mine.length:'';
275
- if(!mine.length){ body.innerHTML='<div class="center">No open tasks assigned to '+esc(u.name)+'.</div>'; return; }
276
- const rows=mine.map(t=>`
277
- <tr><td>${statusDot(t.status)}<span class="title">${esc(t.name||'(task)')}</span></td>
278
- <td>${esc(t.matterNum||'—')}${t.client&&t.client!=='Unassigned'?'<div class="meta">'+esc(t.client)+'</div>':''}</td>
279
- <td>${dueCell(t._d)}</td>
280
- <td style="text-align:right;white-space:nowrap">
281
- <button class="cbtn" onclick="completeTask('${esc(t.id)}',this)"${t.id?'':' disabled title="No task id available"'}>✓ Done</button>
282
- <a class="lk" style="margin-left:12px" href="${esc(t.url)}" target="_blank" rel="noopener">Open ↗</a>
283
- </td></tr>`).join('');
284
- body.innerHTML=`<table class="atable">
285
- <thead><tr><th>Task</th><th>Matter</th><th>Due</th><th style="text-align:right">Action</th></tr></thead>
286
- <tbody>${rows}</tbody></table>`;
287
- }
288
-
289
- function fillSelect(el,items,valKey,labelFn,saved){
290
- el.innerHTML=items.map(i=>`<option value="${esc(i[valKey])}">${esc(labelFn(i))}</option>`).join('');
291
- if(saved&&items.some(i=>i[valKey]===saved)) el.value=saved;
292
- }
293
-
294
- // Resolve the Claude-set DEFAULT_USER (initials or full name) to a userId.
295
- function resolveDefaultUser(){
296
- const d=String(DEFAULT_USER||'').trim().toLowerCase();
297
- if(!d) return null;
298
- const m=USERS.find(u=>(u.initials||'').toLowerCase()===d)||USERS.find(u=>(u.name||'').toLowerCase()===d);
299
- return m?m.userId:null;
300
- }
301
-
302
- function mapTasks(res){
303
- const pc={}; PROJECTS.forEach(p=>{pc[p.projectId]=p.clientName;});
304
- TASKS=asArray(res,'tasks').map(t=>{const d=pd(t.dueDate);return {
305
- id:t._id||t.taskId||t.id||'',
306
- name:t.taskName||t.name||'(task)', matterNum:t.matterNum||'',
307
- client:pc[t.projectId]||t.clientName||'Unassigned',
308
- assigneeId:(t.assignee&&t.assignee.userId)||'',
309
- status:t.status||'active', projectId:t.projectId, url:BASE+(t.projectId||''),
310
- _d:d, _dl:dleft(d)
311
- };});
312
- }
313
-
314
- // Fetch only the selected user's open tasks via the server-side assigneeId
315
- // filter (small, exact payload — no firm-wide pull, no initials matching).
316
- async function loadTasks(userId){
317
- const body=document.getElementById('tasksBody');
318
- TASKS=[];
319
- if(!userId){ body.dataset.loading='0'; renderTasks(userId); return; }
320
- body.dataset.loading='1'; renderTasks(userId); // show spinner
321
- const res=await call('list_open_tasks',{assigneeId:userId});
322
- body.dataset.loading='0';
323
- if(res===null){ TASKS=null; renderTasks(userId); return; } // fetch failed
324
- mapTasks(res);
325
- renderKpis(USERS.find(u=>u.userId===userId));
326
- renderTasks(userId);
327
- }
328
-
329
- async function boot(){
330
- document.getElementById('prog').innerHTML=
331
- '<span id="chip-users" class="chip"><span class="sp"></span>Team</span>'+
332
- '<span id="chip-projects" class="chip"><span class="sp"></span>Active projects</span>';
333
- renderKpis(null);
334
-
335
- const [uRes,pRes]=await Promise.all([call('list_users'),call('list_active_projects')]);
336
- USERS=asArray(uRes).filter(u=>u.name&&u.name!=='Placeholder');
337
- PROJECTS=asArray(pRes);
338
- setChip('users',USERS.length?'done':'err',USERS.length+' members');
339
- setChip('projects',PROJECTS.length?'done':'err',PROJECTS.length+' projects');
340
-
341
- USERS.sort((a,b)=>(b.totalEstimatedHours||0)-(a.totalEstimatedHours||0)||(a.name||'').localeCompare(b.name||''));
342
-
343
- const uSel=document.getElementById('userSel');
344
- const savedU=localStorage.getItem('fx_user');
345
- // First load uses Claude's DEFAULT_USER; a saved pick overrides it thereafter.
346
- const initialSel=(savedU&&USERS.some(u=>u.userId===savedU))?savedU:resolveDefaultUser();
347
- fillSelect(uSel,USERS,'userId',u=>`${u.name}${u.initials?' ('+u.initials+')':''} — ${u.totalActiveProjects||0} proj`,initialSel);
348
-
349
- // Selecting a member fetches just that person's open tasks (server-side filter).
350
- async function select(userId){ await renderProfile(userId); await loadTasks(userId); }
351
-
352
- uSel.onchange=async()=>{ localStorage.setItem('fx_user',uSel.value); syncReset(); await select(uSel.value); };
353
-
354
- // Reset-to-default link: clears the saved pick so DEFAULT_USER wins again.
355
- const reset=document.getElementById('resetDef');
356
- function syncReset(){ reset.style.display=(DEFAULT_USER&&localStorage.getItem('fx_user'))?'':'none'; }
357
- reset.onclick=async(e)=>{ e.preventDefault(); localStorage.removeItem('fx_user');
358
- const d=resolveDefaultUser(); if(d) uSel.value=d; syncReset(); await select(uSel.value); };
359
- syncReset();
360
-
361
- document.getElementById('sub').textContent='Live · '+USERS.length+' team members';
362
- await select(uSel.value);
363
- }
364
- boot().catch(e=>{ document.getElementById('profilePanel').innerHTML=
365
- '<h3>Profile</h3><div class="center">Failed to load Fenix data.</div>'; });
366
- </script>
367
- </body>
368
- </html>
@@ -1 +0,0 @@
1
- place custom instructions here