This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "ダッシュボード",
|
||||
"description": "output/ を集計して表示",
|
||||
"entry": "index.html"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"app": "dashboard",
|
||||
"seed_files": [
|
||||
{ "path": "output/a.txt", "content": "x\ny" },
|
||||
{ "path": "output/b.txt", "content": "z" }
|
||||
],
|
||||
"steps": [
|
||||
{ "type": "click", "selector": "[data-testid=\"refresh\"]" },
|
||||
{ "type": "getText", "selector": "[data-testid=\"summary\"]" }
|
||||
],
|
||||
"expect": [
|
||||
{ "kind": "text", "target": "[data-testid=\"summary\"]", "contains": "ファイル数" },
|
||||
{ "kind": "text", "target": "[data-testid=\"status\"]", "contains": "更新OK" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<!doctype html><html lang="ja"><head><meta charset="utf-8" />
|
||||
<style>
|
||||
body{font:14px system-ui;margin:0;padding:16px}
|
||||
.row{display:flex;gap:8px;align-items:center;margin-bottom:12px}
|
||||
#summary{font-size:16px;font-weight:bold;margin-bottom:16px;min-height:24px}
|
||||
.bar-wrap{margin-bottom:16px}
|
||||
.bar-label{font-size:12px;margin-bottom:2px;color:#444}
|
||||
svg.bar{display:block;width:100%;max-width:400px;height:20px;border-radius:4px;overflow:hidden;background:#eee}
|
||||
rect.bar-fill{fill:#2563eb}
|
||||
span[data-testid="status"]{font-size:12px;color:#555}
|
||||
button{padding:6px 16px;border-radius:4px;border:none;background:#2563eb;color:#fff;cursor:pointer}
|
||||
button:hover{background:#1d4ed8}
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="row">
|
||||
<button id="refreshBtn" data-testid="refresh">更新</button>
|
||||
<span id="status" data-testid="status"></span>
|
||||
</div>
|
||||
<div id="summary" data-testid="summary"></div>
|
||||
<div id="bars"></div>
|
||||
<script>
|
||||
let seq=0;const pending=new Map();
|
||||
function call(req){return new Promise((res,rej)=>{const id=++seq;pending.set(id,{res,rej});
|
||||
window.parent.postMessage({...req,id},'*');});}
|
||||
window.addEventListener('message',e=>{const r=e.data;const p=pending.get(r&&r.id);if(!p)return;
|
||||
pending.delete(r.id);r.ok?p.res(r.data):p.rej(new Error(r.error));});
|
||||
const $=id=>document.getElementById(id);
|
||||
|
||||
const TEXT_EXTS=['.txt','.md','.csv','.json','.jsonl','.log','.yaml','.yml'];
|
||||
function isText(name){return TEXT_EXTS.some(e=>name.endsWith(e));}
|
||||
|
||||
async function refresh(){
|
||||
$('status').textContent='集計中…';
|
||||
$('summary').textContent='';
|
||||
$('bars').innerHTML='';
|
||||
try{
|
||||
const d=await call({type:'listFiles',dir:'output'});
|
||||
const entries=d.entries||[];
|
||||
const count=entries.length;
|
||||
const lineCounts=[];
|
||||
for(const name of entries){
|
||||
if(!isText(name)){lineCounts.push({name,lines:null});continue;}
|
||||
try{
|
||||
const fd=await call({type:'readFile',path:'output/'+name});
|
||||
const lines=(fd.content||'').split('\n').filter(l=>l.length>0).length;
|
||||
lineCounts.push({name,lines});
|
||||
}catch(_){lineCounts.push({name,lines:null});}
|
||||
}
|
||||
$('summary').textContent='ファイル数: '+count;
|
||||
const withLines=lineCounts.filter(x=>x.lines!==null);
|
||||
const maxLines=Math.max(...withLines.map(x=>x.lines),1);
|
||||
withLines.forEach(({name,lines})=>{
|
||||
const wrap=document.createElement('div');wrap.className='bar-wrap';
|
||||
const lbl=document.createElement('div');lbl.className='bar-label';
|
||||
lbl.textContent=name+' ('+lines+'行)';
|
||||
const pct=Math.round((lines/maxLines)*100);
|
||||
const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');
|
||||
svg.setAttribute('class','bar');svg.setAttribute('viewBox','0 0 100 20');
|
||||
svg.setAttribute('preserveAspectRatio','none');
|
||||
const rect=document.createElementNS('http://www.w3.org/2000/svg','rect');
|
||||
rect.setAttribute('class','bar-fill');rect.setAttribute('x','0');rect.setAttribute('y','0');
|
||||
rect.setAttribute('width',String(pct));rect.setAttribute('height','20');
|
||||
svg.appendChild(rect);wrap.appendChild(lbl);wrap.appendChild(svg);
|
||||
$('bars').appendChild(wrap);
|
||||
});
|
||||
$('status').textContent='更新OK';
|
||||
}catch(err){$('status').textContent='エラー:'+err.message;}
|
||||
}
|
||||
|
||||
$('refreshBtn').onclick=refresh;
|
||||
refresh();
|
||||
</script></body></html>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "データビューア",
|
||||
"description": "output/ のファイルを一覧・閲覧",
|
||||
"entry": "index.html"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"app": "data-viewer",
|
||||
"seed_files": [
|
||||
{ "path": "output/data.csv", "content": "name,age\nalice,30\nbob,25" }
|
||||
],
|
||||
"steps": [
|
||||
{ "type": "click", "selector": "[data-testid=\"list\"]" },
|
||||
{ "type": "getText", "selector": "[data-testid=\"files\"]" }
|
||||
],
|
||||
"expect": [
|
||||
{ "kind": "text", "target": "[data-testid=\"files\"]", "contains": "data.csv" },
|
||||
{ "kind": "text", "target": "[data-testid=\"status\"]", "contains": "取得OK" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<!doctype html><html lang="ja"><head><meta charset="utf-8" />
|
||||
<style>
|
||||
body{font:14px system-ui;margin:0;padding:16px}
|
||||
.row{display:flex;gap:8px;align-items:center;margin-bottom:8px}
|
||||
ul{list-style:none;padding:0;margin:0}
|
||||
li[data-testid="file-item"]{cursor:pointer;padding:4px 8px;border-radius:4px;background:#f0f0f0;margin-bottom:4px}
|
||||
li[data-testid="file-item"]:hover{background:#dce8ff}
|
||||
pre[data-testid="preview"]{background:#f9f9f9;border:1px solid #ddd;padding:12px;overflow:auto;max-height:50vh;white-space:pre-wrap}
|
||||
table{border-collapse:collapse;width:100%}
|
||||
table th,table td{border:1px solid #ccc;padding:4px 8px;text-align:left}
|
||||
table th{background:#eee}
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="row">
|
||||
<input id="dir" data-testid="dir" value="output" style="flex:1" />
|
||||
<button id="listBtn" data-testid="list">一覧取得</button>
|
||||
<span id="status" data-testid="status"></span>
|
||||
</div>
|
||||
<ul id="files" data-testid="files"></ul>
|
||||
<div id="preview-wrap"></div>
|
||||
<script>
|
||||
let seq=0;const pending=new Map();
|
||||
function call(req){return new Promise((res,rej)=>{const id=++seq;pending.set(id,{res,rej});
|
||||
window.parent.postMessage({...req,id},'*');});}
|
||||
window.addEventListener('message',e=>{const r=e.data;const p=pending.get(r&&r.id);if(!p)return;
|
||||
pending.delete(r.id);r.ok?p.res(r.data):p.rej(new Error(r.error));});
|
||||
const $=id=>document.getElementById(id);
|
||||
|
||||
function renderCsv(text){
|
||||
const rows=text.trim().split('\n').map(r=>r.split(','));
|
||||
const table=document.createElement('table');
|
||||
rows.forEach((cols,i)=>{const tr=document.createElement('tr');
|
||||
cols.forEach(c=>{const cell=document.createElement(i===0?'th':'td');cell.textContent=c;tr.appendChild(cell);});
|
||||
table.appendChild(tr);});
|
||||
return table;
|
||||
}
|
||||
|
||||
async function listDir(){
|
||||
const dir=$('dir').value.trim()||'output';
|
||||
$('status').textContent='取得中…';
|
||||
$('files').innerHTML='';
|
||||
$('preview-wrap').innerHTML='';
|
||||
try{
|
||||
const d=await call({type:'listFiles',dir});
|
||||
const entries=d.entries||[];
|
||||
if(entries.length===0){$('files').innerHTML='<li>(ファイルなし)</li>';}
|
||||
entries.forEach(name=>{
|
||||
const li=document.createElement('li');
|
||||
li.setAttribute('data-testid','file-item');
|
||||
li.textContent=name;
|
||||
li.onclick=()=>openFile(dir,name);
|
||||
$('files').appendChild(li);
|
||||
});
|
||||
$('status').textContent='取得OK';
|
||||
}catch(err){$('status').textContent='エラー:'+err.message;}
|
||||
}
|
||||
|
||||
async function openFile(dir,name){
|
||||
$('status').textContent='読込中…';
|
||||
$('preview-wrap').innerHTML='';
|
||||
try{
|
||||
const path=dir.replace(/\/$/,'')+'/'+name;
|
||||
const d=await call({type:'readFile',path});
|
||||
const content=d.content||'';
|
||||
if(name.endsWith('.csv')){
|
||||
const wrap=document.createElement('div');
|
||||
wrap.setAttribute('data-testid','preview');
|
||||
wrap.appendChild(renderCsv(content));
|
||||
$('preview-wrap').appendChild(wrap);
|
||||
}else{
|
||||
const pre=document.createElement('pre');
|
||||
pre.setAttribute('data-testid','preview');
|
||||
pre.textContent=content;
|
||||
$('preview-wrap').appendChild(pre);
|
||||
}
|
||||
$('status').textContent='読込OK';
|
||||
}catch(err){$('status').textContent='読込失敗:'+err.message;}
|
||||
}
|
||||
|
||||
$('listBtn').onclick=listDir;
|
||||
</script></body></html>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "フォーム入力",
|
||||
"description": "入力を output/ に構造化保存",
|
||||
"entry": "index.html"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"app": "form-input",
|
||||
"seed_files": [],
|
||||
"steps": [
|
||||
{ "type": "fill", "selector": "[data-testid=\"f-name\"]", "value": "alice" },
|
||||
{ "type": "fill", "selector": "[data-testid=\"f-note\"]", "value": "hello-e2e" },
|
||||
{ "type": "click", "selector": "[data-testid=\"submit\"]" },
|
||||
{ "type": "getText", "selector": "[data-testid=\"status\"]" }
|
||||
],
|
||||
"expect": [
|
||||
{ "kind": "text", "target": "[data-testid=\"status\"]", "contains": "保存" },
|
||||
{ "kind": "file", "target": "output/submissions.jsonl", "contains": "hello-e2e" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<!doctype html><html lang="ja"><head><meta charset="utf-8" />
|
||||
<style>
|
||||
body{font:14px system-ui;margin:0;padding:16px}
|
||||
.row{display:flex;gap:8px;align-items:center;margin-bottom:8px}
|
||||
input[type="text"],textarea{border:1px solid #ccc;border-radius:4px;padding:6px 8px;width:100%;box-sizing:border-box}
|
||||
label{display:block;font-weight:bold;margin-bottom:4px;margin-top:12px}
|
||||
button{padding:6px 16px;border-radius:4px;border:none;background:#2563eb;color:#fff;cursor:pointer}
|
||||
button:hover{background:#1d4ed8}
|
||||
span[data-testid="status"]{font-size:12px;color:#555}
|
||||
</style></head>
|
||||
<body>
|
||||
<label for="f-name">名前</label>
|
||||
<input type="text" id="f-name" data-testid="f-name" placeholder="名前を入力" />
|
||||
<label for="f-note">メモ</label>
|
||||
<input type="text" id="f-note" data-testid="f-note" placeholder="メモを入力" />
|
||||
<div class="row" style="margin-top:16px">
|
||||
<button id="submitBtn" data-testid="submit">保存</button>
|
||||
<span id="status" data-testid="status"></span>
|
||||
</div>
|
||||
<script>
|
||||
let seq=0;const pending=new Map();
|
||||
function call(req){return new Promise((res,rej)=>{const id=++seq;pending.set(id,{res,rej});
|
||||
window.parent.postMessage({...req,id},'*');});}
|
||||
window.addEventListener('message',e=>{const r=e.data;const p=pending.get(r&&r.id);if(!p)return;
|
||||
pending.delete(r.id);r.ok?p.res(r.data):p.rej(new Error(r.error));});
|
||||
const $=id=>document.getElementById(id);
|
||||
|
||||
const DEST='output/submissions.jsonl';
|
||||
|
||||
async function submit(){
|
||||
const name=$('f-name').value.trim();
|
||||
const note=$('f-note').value.trim();
|
||||
if(!name&&!note){$('status').textContent='入力してください';return;}
|
||||
$('status').textContent='保存中…';
|
||||
try{
|
||||
let existing='';
|
||||
try{const d=await call({type:'readFile',path:DEST});existing=d.content||'';}catch(_){}
|
||||
const line=JSON.stringify({name,note,ts:new Date().toISOString()});
|
||||
const updated=(existing.endsWith('\n')||existing==='')
|
||||
?existing+line+'\n'
|
||||
:existing+'\n'+line+'\n';
|
||||
await call({type:'writeFile',path:DEST,content:updated});
|
||||
$('f-name').value='';$('f-note').value='';
|
||||
$('status').textContent='保存しました';
|
||||
}catch(err){$('status').textContent='保存失敗:'+err.message;}
|
||||
}
|
||||
|
||||
$('submitBtn').onclick=submit;
|
||||
</script></body></html>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "ノートエディタ",
|
||||
"description": "output/ のテキストを編集して保存",
|
||||
"entry": "index.html"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"app": "note-editor",
|
||||
"seed_files": [{ "path": "output/note.md", "content": "seeded-content" }],
|
||||
"steps": [
|
||||
{ "type": "click", "selector": "[data-testid=\"load\"]" },
|
||||
{ "type": "getText", "selector": "[data-testid=\"status\"]" },
|
||||
{ "type": "fill", "selector": "[data-testid=\"body\"]", "value": "edited-by-e2e" },
|
||||
{ "type": "click", "selector": "[data-testid=\"save\"]" },
|
||||
{ "type": "getText", "selector": "[data-testid=\"status\"]" }
|
||||
],
|
||||
"expect": [
|
||||
{ "kind": "text", "target": "[data-testid=\"status\"]", "contains": "保存OK" },
|
||||
{ "kind": "file", "target": "output/note.md", "contains": "edited-by-e2e" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<!doctype html><html lang="ja"><head><meta charset="utf-8" />
|
||||
<style>body{font:14px system-ui;margin:0;padding:16px}textarea{width:100%;height:60vh}
|
||||
.row{display:flex;gap:8px;align-items:center;margin-bottom:8px}</style></head>
|
||||
<body>
|
||||
<div class="row">
|
||||
<input id="path" data-testid="path" value="output/note.md" style="flex:1" />
|
||||
<button id="load" data-testid="load">読み込み</button>
|
||||
<button id="save" data-testid="save">保存</button>
|
||||
<span id="status" data-testid="status"></span>
|
||||
</div>
|
||||
<textarea id="body" data-testid="body"></textarea>
|
||||
<script>
|
||||
let seq=0;const pending=new Map();
|
||||
function call(req){return new Promise((res,rej)=>{const id=++seq;pending.set(id,{res,rej});
|
||||
window.parent.postMessage({...req,id},'*');});}
|
||||
window.addEventListener('message',e=>{const r=e.data;const p=pending.get(r&&r.id);if(!p)return;
|
||||
pending.delete(r.id);r.ok?p.res(r.data):p.rej(new Error(r.error));});
|
||||
const $=id=>document.getElementById(id);
|
||||
async function load(){try{const d=await call({type:'readFile',path:$('path').value});
|
||||
$('body').value=d.content;$('status').textContent='読込OK';}catch(err){$('status').textContent='読込失敗:'+err.message;}}
|
||||
async function save(){try{await call({type:'writeFile',path:$('path').value,content:$('body').value});
|
||||
$('status').textContent='保存OK';}catch(err){$('status').textContent='保存失敗:'+err.message;}}
|
||||
$('load').onclick=load;$('save').onclick=save;
|
||||
load();
|
||||
</script></body></html>
|
||||
Reference in New Issue
Block a user