<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ๅ่ตไบง</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f0f1a; color: #e0e0e0; height: 100vh; overflow: hidden; }
.header { background: #1a1a2e; padding: 12px 20px; border-bottom: 1px solid #2a2a4a; display: flex; align-items: center; gap: 12px; }
.header h1 { font-size: 16px; color: #FFD700; }
.header .subtitle { font-size: 12px; color: #888; }
.container { display: flex; height: calc(100vh - 50px); }
.panel { display: flex; flex-direction: column; }
.panel-left { width: 280px; min-width: 240px; border-right: 1px solid #2a2a4a; }
.panel-mid { width: 260px; min-width: 220px; border-right: 1px solid #2a2a4a; }
.panel-skills { width: 340px; min-width: 280px; border-right: 1px solid #2a2a4a; }
.panel-right { flex: 1; }
.panel-header { padding: 8px 14px; background: #16162a; border-bottom: 1px solid #2a2a4a; display: flex; justify-content: space-between; align-items: center; }
.panel-header h2 { font-size: 13px; color: #aaa; }
.panel-header .count { font-size: 11px; color: #666; background: #1e1e3a; padding: 2px 8px; border-radius: 8px; }
.list-area { flex: 1; overflow-y: auto; padding: 4px 0; }
.search-box { padding: 8px 14px; background: #16162a; border-bottom: 1px solid #2a2a4a; }
.search-box input { width: 100%; padding: 6px 10px; border: 1px solid #2a2a4a; border-radius: 4px; background: #1a1a30; color: #e0e0e0; font-size: 12px; outline: none; }
.search-box input:focus { border-color: #FFD700; }
/* ็ฎๅฝๆ */
.dir { user-select: none; }
.dir-header { padding: 5px 10px 5px 14px; cursor: pointer; display: flex; align-items: center; gap: 4px; font-size: 12px; color: #aaa; border-radius: 3px; }
.dir-header:hover { background: #1e1e38; }
.dir-header .toggle { width: 14px; font-size: 10px; color: #666; }
.dir-header .dname { flex: 1; }
.dir-header .dcount { font-size: 10px; color: #555; }
.dir-children { display: none; }
.dir.open > .dir-children { display: block; }
.dir-actions { display: none; margin-left: auto; gap: 2px; }
.dir-header:hover .dir-actions { display: flex; }
.dir-actions span { font-size: 10px; color: #666; padding: 0 4px; cursor: pointer; border-radius: 2px; }
.dir-actions span:hover { background: #2a2a4a; color: #FFD700; }
/* task items */
.task-item { padding: 5px 10px 5px 14px; cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 6px; border-left: 2px solid transparent; }
.task-item:hover { background: #1e1e38; }
.task-item.active { background: #252550; border-left-color: #FFD700; }
.task-item .tname { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-actions { display: none; gap: 2px; flex-shrink: 0; }
.task-item:hover .task-actions { display: flex; }
.task-actions span { font-size: 10px; color: #666; padding: 0 4px; cursor: pointer; border-radius: 2px; }
.task-actions span:hover { background: #2a2a4a; color: #FFD700; }
.task-actions .del:hover { color: #ef4444; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
.dot-active { background: #4caf50; }
.dot-running { background: #ffa726; }
.dot-created { background: #666; }
.dot-completed { background: #888; }
/* skill items */
.skill-item { padding: 6px 12px; font-size: 12px; border-bottom: 1px solid #1e1e30; }
.skill-item:hover { background: #1a1a30; }
.skill-header { display: flex; align-items: center; gap: 6px; }
.skill-name { font-weight: bold; color: #e0e0e0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.skill-cat { font-size: 10px; color: #888; background: #1e1e30; padding: 1px 6px; border-radius: 4px; }
.skill-desc { font-size: 11px; color: #888; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.skill-tasks { margin-top: 4px; display: flex; flex-wrap: wrap; gap: 3px; }
.skill-task-badge { font-size: 10px; padding: 1px 6px; border-radius: 4px; background: #2a3a5a; color: #7eb8ff; white-space: nowrap; }
.skill-task-badge-none { background: #2a2a2a; color: #666; cursor: pointer; }
.skill-task-badge-none:hover { background: #3a2a2a; color: #ff7eb8; }
.skill-actions { margin-top: 4px; display: flex; gap: 4px; }
.btn-assign { font-size: 10px; padding: 2px 10px; border: 1px solid #3a5a3a; border-radius: 4px; background: #1a2a1a; color: #4ade80; cursor: pointer; }
.btn-assign:hover { background: #2a4a2a; }
.skill-assign-form { margin-top: 4px; display: flex; gap: 4px; }
.skill-assign-form select { flex: 1; padding: 3px 6px; border: 1px solid #2a2a4a; border-radius: 3px; background: #1a1a30; color: #e0e0e0; font-size: 11px; outline: none; }
.skill-assign-form select:focus { border-color: #FFD700; }
.skill-assign-form .btn-confirm { font-size: 10px; padding: 2px 8px; border: none; border-radius: 3px; background: #FFD700; color: #1a1a2e; cursor: pointer; }
.skill-category-group { font-size: 11px; color: #888; padding: 4px 12px; background: #131325; border-bottom: 1px solid #1e1e30; display: flex; align-items: center; gap: 6px; }
.skill-category-group .cat-toggle { cursor: pointer; font-size: 10px; }
.skill-category-group .cat-count { font-size: 10px; color: #555; }
/* doc items */
.doc-item { padding: 6px 14px; cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 8px; border-left: 2px solid transparent; }
.doc-item:hover { background: #1e1e38; }
.doc-item .doc-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #ccc; }
.doc-item .doc-actions { display: none; gap: 4px; flex-shrink: 0; }
.doc-item:hover .doc-actions { display: flex; }
.doc-actions span { font-size: 10px; color: #888; padding: 2px 6px; cursor: pointer; border-radius: 3px; }
.doc-actions span:hover { background: #2a2a4a; color: #FFD700; }
.task-badge { font-size: 10px; padding: 1px 8px; border-radius: 8px; background: #2a3a5a; color: #7eb8ff; white-space: nowrap; flex-shrink: 0; max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
.task-badge-none { background: #2a2a2a; color: #666; }
.doc-task-select { width: 100%; padding: 8px 10px; border: 1px solid #2a2a4a; border-radius: 4px; background: #1a1a30; color: #e0e0e0; font-size: 13px; margin-bottom: 12px; }
.doc-task-select:focus { border-color: #FFD700; outline: none; }
.doc-task-label { font-size: 11px; color: #888; margin-bottom: 6px; }
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ๆฝๅฑ้ข่ง
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.drawer-overlay {
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.55); z-index: 200;
}
.drawer {
position: fixed; top: 0; right: -55vw; width: 55vw; height: 100vh;
background: #14142a; border-left: 1px solid #2a2a4a;
z-index: 201; transition: right 0.28s ease;
display: flex; flex-direction: column;
box-shadow: -4px 0 20px rgba(0,0,0,0.4);
}
.drawer.open { right: 0; }
.drawer-header {
display: flex; align-items: center; padding: 12px 18px;
border-bottom: 1px solid #2a2a4a; flex-shrink: 0;
}
.drawer-title { flex: 1; font-size: 15px; font-weight: 600; color: #FFD700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drawer-close { font-size: 18px; color: #888; cursor: pointer; padding: 4px 8px; border-radius: 4px; }
.drawer-close:hover { background: #2a2a4a; color: #FFD700; }
.drawer-content {
flex: 1; overflow-y: auto; padding: 18px;
font-size: 14px; line-height: 1.7; color: #d0d0d0;
}
.drawer-content h1 { font-size: 20px; color: #e0e0e0; margin: 16px 0 10px; padding-bottom: 6px; border-bottom: 1px solid #2a2a4a; }
.drawer-content h2 { font-size: 17px; color: #e0e0e0; margin: 14px 0 8px; }
.drawer-content h3 { font-size: 15px; color: #ccc; margin: 12px 0 6px; }
.drawer-content p { margin: 8px 0; }
.drawer-content ul, .drawer-content ol { margin: 8px 0 8px 20px; }
.drawer-content li { margin: 4px 0; }
.drawer-content code { background: #1e1e38; padding: 2px 6px; border-radius: 3px; font-size: 13px; color: #cba6f7; }
.drawer-content pre { background: #0d0d1a; padding: 14px 16px; border-radius: 6px; overflow-x: auto; margin: 10px 0; border: 1px solid #2a2a4a; font-size: 13px; }
.drawer-content pre code { background: none; padding: 0; color: inherit; }
.drawer-content blockquote { border-left: 3px solid #FFD700; padding: 6px 14px; margin: 10px 0; background: #1a1a30; color: #aaa; }
.drawer-content a { color: #7eb8ff; }
.drawer-content a:hover { text-decoration: underline; }
.drawer-content hr { border: none; border-top: 1px solid #2a2a4a; margin: 16px 0; }
.drawer-content table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 13px; }
.drawer-content th, .drawer-content td { border: 1px solid #2a2a4a; padding: 6px 10px; text-align: left; }
.drawer-content th { background: #1a1a30; color: #ccc; }
.drawer-content tr:nth-child(even) { background: #12122a; }
.drawer-content img { max-width: 100%; border-radius: 6px; margin: 10px 0; }
.drawer-content .yaml-frontmatter { display: none; }
.drawer-loading { padding: 40px; text-align: center; color: #666; font-size: 14px; }
.drawer-skill-link { font-size: 11px; color: #888; padding: 4px 14px; background: #1a1a30; border-radius: 4px; cursor: pointer; display: inline-block; }
.drawer-skill-link:hover { background: #2a2a4a; color: #FFD700; }
.dir-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; background: #1e2a3a; color: #7eb8ff; white-space: nowrap; flex-shrink: 0; max-width: 160px; overflow: hidden; text-overflow: ellipsis; margin-left: 6px; cursor: default; }
.dir-badge-none { background: #2a2a2a; color: #666; }
.dir-badge:hover { background: #2a3a4a; }
.btn-sm { padding: 3px 10px; border: 1px solid #2a2a4a; border-radius: 3px; background: #1a1a30; color: #aaa; cursor: pointer; font-size: 11px; }
.btn-sm:hover { background: #2a2a4a; color: #FFD700; }
.btn-primary { background: #FFD700; color: #1a1a2e; border: none; padding: 4px 14px; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: bold; }
.btn-primary:hover { background: #FFC000; }
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 100; }
.modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); background: #1a1a2e; padding: 20px; border-radius: 8px; border: 1px solid #2a2a4a; z-index: 101; min-width: 300px; max-width: 500px; }
.modal h3 { font-size: 14px; margin-bottom: 12px; }
.modal input, .modal select { width: 100%; padding: 8px 10px; border: 1px solid #2a2a4a; border-radius: 4px; background: #1a1a30; color: #e0e0e0; font-size: 13px; margin-bottom: 12px; }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; }
.toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #2a2a4a; color: #e0e0e0; padding: 8px 20px; border-radius: 6px; font-size: 13px; display: none; z-index: 200; }
.dir-empty { padding: 8px 28px; font-size: 11px; color: #444; }
.move-dir-select { width: 100%; padding: 8px 10px; border: 1px solid #2a2a4a; border-radius: 4px; background: #1a1a30; color: #e0e0e0; font-size: 13px; margin-bottom: 12px; }
.move-dir-select:focus { border-color: #FFD700; outline: none; }
.skill-search { margin-bottom: 0; border-bottom: 1px solid #2a2a4a; }
</style>
</head>
<body>
<div class="header">
<h1>๐๏ธ ๅ่ตไบง</h1>
<span class="subtitle">็ฎๅฝ ยท Task ยท Skill ยท ๆๆกฃ</span>
<span style="margin-left:auto;display:flex;gap:8px;">
<button class="btn-sm" onclick="showCreateTask()">+ ๆฐๅปบ Task</button>
<button class="btn-sm" onclick="showCreateDir()">+ ๆฐๅปบ็ฎๅฝ</button>
<span class="count" id="totalCount">0</span>
</span>
</div>
<div class="container">
<!-- ๅ1๏ผ็ฎๅฝๆ -->
<div class="panel panel-left">
<div class="panel-header"><h2>๐ ็ฎๅฝๆ </h2><span class="count" id="dirCount">0</span></div>
<div class="list-area" id="dirTree"></div>
<div class="search-box">
<input type="text" id="taskSearch" placeholder="ๆ็ดขtask..." oninput="onSearchInput()">
</div>
</div>
<!-- ๅ2๏ผTaskๅ่กจ -->
<div class="panel panel-mid">
<div class="panel-header">
<h2 id="selDirTitle">๐ Task ๅ่กจ</h2>
<span class="count" id="selDirCount">0</span>
</div>
<div class="list-area" id="taskList">
<div style="padding:20px;text-align:center;color:#555;">โ ้ๆฉไธไธช็ฎๅฝ</div>
</div>
</div>
<!-- ๅ3๏ผๆ่ฝ็ฎก็ -->
<div class="panel panel-skills">
<div class="panel-header">
<h2>โก Skill ๅฝๅฑ็ฎก็</h2>
<span class="count" id="skillCount">0</span>
</div>
<div class="search-box skill-search">
<input type="text" id="skillSearch" placeholder="ๆ็ดขskill..." oninput="renderSkills()">
</div>
<div class="list-area" id="skillList">
<div style="padding:20px;text-align:center;color:#555;">ๅ ่ฝฝไธญ...</div>
</div>
</div>
<!-- ๅ4๏ผๆๆกฃๅ่กจ -->
<div class="panel panel-right">
<div class="panel-header">
<h2>๐ ๆๆกฃ ยท ๅฝๅฑTask</h2>
<span class="count" id="docCount">0</span>
</div>
<div id="docPanel" style="padding:20px;text-align:center;color:#555;">โ ้ๆฉไธไธชTask</div>
</div>
</div>
<!-- Modal -->
<div class="modal-overlay" id="modalOverlay" onclick="closeModal()"></div>
<div class="modal" id="modal" style="display:none;">
<h3 id="modalTitle">ๆฐๅปบ็ฎๅฝ</h3>
<input type="text" id="modalInput" placeholder="็ฎๅฝๅ็งฐ">
<div id="modalInputContainer"></div>
<div style="font-size:11px;color:#555;margin-bottom:8px;">ไธ็บง็ฎๅฝ: <span id="modalParent"></span></div>
<div class="modal-actions">
<button class="btn-sm" onclick="closeModal()">ๅๆถ</button>
<button class="btn-primary" id="modalConfirm" onclick="confirmModal()">็กฎๅฎ</button>
</div>
</div>
<div class="toast" id="toast"></div>
<!-- Drawer -->
<div class="drawer-overlay" id="drawerOverlay" onclick="closeDrawer()"></div>
<div class="drawer" id="drawer">
<div class="drawer-header">
<span class="drawer-title" id="drawerTitle"></span>
<span class="drawer-close" onclick="closeDrawer()">โ</span>
</div>
<div class="drawer-content" id="drawerContent"></div>
</div>
<script src="/chat/marked.umd.js"></script>
<script src="/task-selector.js"></script>
<script>
let allTasks = [];
let allDirs = [];
let allTasksList = [];
let allSkills = [];
let allTaskListForSelect = [];
let selectedDirId = null;
let selectedTaskId = null;
let openDirIds = new Set();
let modalMode = 'create';
let modalContext = {};
let editingDocId = null;
let openSkillCat = new Set();
async function api(url, method, body) {
const opts = { method, headers:{'Content-Type':'application/json'} };
if (body) opts.body = JSON.stringify(body);
const r = await fetch(url, opts);
return r.json();
}
async function init() {
await loadData();
}
async function loadData() {
const [taskData, dirData, tasksData, skillsData] = await Promise.all([
api('/api/tasks-with-folder'),
api('/api/task-dirs'),
api('/api/tasks'),
api('/api/skills')
]);
allTasks = (taskData.tasks || []).sort((a,b) => a.name.localeCompare(b.name));
allDirs = dirData.dirs || [];
allTasksList = tasksData || [];
allSkills = skillsData || [];
allTaskListForSelect = allTasksList;
document.getElementById('totalCount').textContent = allTasks.length + ' tasks';
document.getElementById('dirCount').textContent = allDirs.length + ' ็ฎๅฝ';
document.getElementById('skillCount').textContent = allSkills.length + ' skills';
renderAll();
renderTaskList();
renderSkills();
renderDocList();
}
function getChildren(dirs, parentId) {
return dirs.filter(d => d.parent_id === parentId).sort((a,b) => (a.sort_order||0) - (b.sort_order||0));
}
function getDirPath(dirId) {
const parts = [];
let current = allDirs.find(d => d.id === dirId);
while (current) {
parts.unshift(current.name);
current = allDirs.find(d => d.id === current.parent_id);
}
return parts.join(' / ');
}
function renderDirTree() {
const root = allDirs.find(d => d.parent_id === null);
if (!root) return '<div style="padding:14px;color:#555;">ๆๆ ็ฎๅฝ</div>';
return renderNodes(allDirs, root.id, 0);
}
function renderNodes(dirs, parentId, depth) {
const children = getChildren(dirs, parentId);
if (!children.length) return '';
let html = '';
children.forEach(d => {
const hasChildren = dirs.some(x => x.parent_id === d.id);
const tasksHere = allTasks.filter(t => t.dir_id === d.id);
let subHtml = '';
if (hasChildren) {
subHtml = renderNodes(dirs, d.id, depth + 1);
}
const q = (document.getElementById('taskSearch').value || '').toLowerCase();
const dirMatch = !q || (d.name || '').toLowerCase().includes(q);
const taskMatch = !q || tasksHere.some(t => (t.name || '').toLowerCase().includes(q));
const subMatch = !q || subHtml.trim();
if (q && !dirMatch && !taskMatch && !subMatch) return;
const isOpen = openDirIds.has(d.id) || selectedDirId === d.id ? 'open' : '';
html += `<div class="dir ${isOpen}" data-dir-id="${d.id}">`;
html += `<div class="dir-header" style="padding-left:${14 + depth*14}px" draggable="true"
ondragstart="onDirDragStart(event, '${d.id}')"
ondragover="onDirDragOver(event)" ondragleave="onDirDragLeave(event)" ondrop="onDirDrop(event, '${d.id}')"
onclick="toggleDir(this)">`;
html += `<span class="toggle">โถ</span>`;
html += `<span class="dname">๐ ${d.name}</span>`;
html += `<span class="dcount">${tasksHere.length}</span>`;
html += `<span class="dir-actions">`;
html += `<span onclick="event.stopPropagation();showCreateSubDir('${d.id}','${d.name}')">+</span>`;
html += `<span onclick="event.stopPropagation();showRenameDir('${d.id}','${d.name}')">โ๏ธ</span>`;
html += `<span onclick="event.stopPropagation();deleteDir('${d.id}','${d.name}')">๐</span>`;
html += `</span></div>`;
html += `<div class="dir-children">`;
html += subHtml;
tasksHere.forEach(t => {
if (!matchesSearch(t)) return;
const dot = 'dot-' + (t.status || 'created');
html += `<div class="task-item ${t.id === selectedTaskId ? 'active' : ''}" draggable="true"
ondragstart="onTaskDragStart(event, '${t.id}')"
onclick="selectTask('${t.id}', this)" style="padding-left:${30 + depth*14}px">`;
html += `<span class="status-dot ${dot}"></span>`;
html += `<span class="tname">${t.name}</span>`;
html += `<span class="task-actions">
<span onclick="event.stopPropagation();showMoveTaskDir('${t.id}','${t.name.replace(/'/g, "\\'")}')" title="ไฟฎๆนๅฝๅฑ็ฎๅฝ">๐</span>
<span onclick="event.stopPropagation();showRenameTask('${t.id}','${t.name.replace(/'/g, "\\'")}')" title="้ๅฝๅ">โ๏ธ</span>
<span class="del" onclick="event.stopPropagation();deleteTask('${t.id}','${t.name.replace(/'/g, "\\'")}')" title="ๅ ้ค">๐</span>
</span></div>`;
});
html += `</div></div>`;
});
return html;
}
function renderAll() {
document.getElementById('dirTree').innerHTML = renderDirTree();
}
function getDirBadgeHtml(dirId) {
if (!dirId) return `<span class="dir-badge dir-badge-none" title="ๆชๅฝ็ฑป">๐ ๆชๅ็ฑป</span>`;
const dir = allDirs.find(d => d.id === dirId);
if (!dir) return `<span class="dir-badge dir-badge-none" title="็ฎๅฝๅทฒๅ ้ค">๐ ๆช็ฅ</span>`;
const path = getDirPath(dirId);
return `<span class="dir-badge" title="${path}">๐ ${dir.name}</span>`;
}
function getDirOptionsHtml(selectedDirId) {
const roots = allDirs.filter(d => d.parent_id === null);
let opts = `<option value="">โ ๆชๅ็ฑป โ</option>`;
function addDirOptions(parentId, depth) {
const children = allDirs.filter(d => d.parent_id === parentId).sort((a,b) => (a.sort_order||0)-(b.sort_order||0));
children.forEach(d => {
const sel = d.id === selectedDirId ? ' selected' : '';
opts += `<option value="${d.id}"${sel}>${' '.repeat(depth)}${d.name}</option>`;
addDirOptions(d.id, depth + 1);
});
}
addDirOptions(null, 0);
return opts;
}
function renderTaskList() {
const q = (document.getElementById('taskSearch').value || '').toLowerCase();
const tasks = q ? allTasks.filter(t => t.name.toLowerCase().includes(q)) : allTasks;
document.getElementById('selDirTitle').innerHTML = '๐ ๅ
จ้จ Task';
document.getElementById('selDirCount').textContent = tasks.length;
let html = '';
tasks.forEach(t => {
const dot = 'dot-' + (t.status || 'created');
const dirBadge = getDirBadgeHtml(t.dir_id);
html += `<div class="task-item ${t.id === selectedTaskId ? 'active' : ''}" onclick="selectTask('${t.id}', this)">`;
html += `<span class="status-dot ${dot}"></span>`;
html += `<span class="tname">${t.name}</span>`;
html += dirBadge;
html += `<span class="task-actions">
<span onclick="event.stopPropagation();showMoveTaskDir('${t.id}','${t.name.replace(/'/g, "\\'")}')" title="ไฟฎๆนๅฝๅฑ็ฎๅฝ">๐</span>
<span onclick="event.stopPropagation();showRenameTask('${t.id}','${t.name.replace(/'/g, "\\'")}')" title="้ๅฝๅ">โ๏ธ</span>
<span class="del" onclick="event.stopPropagation();deleteTask('${t.id}','${t.name.replace(/'/g, "\\'")}')" title="ๅ ้ค">๐</span>
</span></div>`;
});
if (!html) html = '<div class="dir-empty">๐ญ ๆๆ task</div>';
document.getElementById('taskList').innerHTML = html;
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๆ่ฝๆธฒๆ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function renderSkills() {
const q = (document.getElementById('skillSearch').value || '').toLowerCase();
// ๆ category ๅ็ป
const groups = {};
allSkills.forEach(s => {
if (q && !s.name.toLowerCase().includes(q) && !(s.description||'').toLowerCase().includes(q)) return;
const cat = s.category || 'ๆชๅ็ฑป';
if (!groups[cat]) groups[cat] = [];
groups[cat].push(s);
});
let html = '';
const catOrder = Object.keys(groups).sort((a,b) => a.localeCompare(b));
catOrder.forEach(cat => {
const skills = groups[cat];
const isOpen = openSkillCat.has(cat);
html += `<div class="skill-category-group">`;
html += `<span class="cat-toggle" onclick="toggleSkillCat('${cat}')">${isOpen ? 'โผ' : 'โถ'}</span>`;
html += `<span>${cat}</span>`;
html += `<span class="cat-count">${skills.length}</span>`;
html += `</div>`;
if (isOpen) {
skills.forEach(s => {
const hasTasks = s.tasks && s.tasks.length > 0;
const taskHtml = hasTasks
? s.tasks.map(t => `<span class="skill-task-badge">๐ ${t.task_name}</span>`).join('')
: `<span class="skill-task-badge skill-task-badge-none" onclick="showSkillAssignForm('${s.name}', this)">+ ๆชๅฝๅฑ</span>`;
html += `<div class="skill-item" data-skill="${s.name}">`;
html += `<div class="skill-header">`;
html += `<span class="skill-name">โก ${s.name}</span>`;
html += `<span class="skill-cat">${s.category || ''}</span>`;
html += `</div>`;
if (s.description) html += `<div class="skill-desc">${s.description}</div>`;
html += `<div class="skill-tasks">${taskHtml}</div>`;
html += `<div class="skill-actions" id="assign-form-${s.name}" style="display:none;">`;
html += `</div>`;
html += `</div>`;
});
}
});
if (!html) html = '<div class="dir-empty">๐ ๆฒกๆๅน้
็ skill</div>';
document.getElementById('skillList').innerHTML = html;
}
function toggleSkillCat(cat) {
if (openSkillCat.has(cat)) openSkillCat.delete(cat);
else openSkillCat.add(cat);
renderSkills();
}
function showSkillAssignForm(skillName, el) {
TaskSelector.show({
title: `โก ้ๆฉใ${skillName}ใ็ๅฝๅฑ Task`,
onSelect: async (task) => {
await api('/api/skill-tasks', 'POST', {
skill_name: skillName,
task_ids: [task.id]
});
showToast(`โ
${skillName} โ ๅทฒๅฝๅฑใ${task.name}ใ`);
await loadData();
}
});
}
async function confirmSkillAssign(skillName) {
// ๆงๆนๆณไฟ็ๅ
ผๅฎน๏ผไฝไธๅ่ขซ่ฐ็จ
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๆๆกฃ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function renderDocList() {
let docs = [];
try {
docs = await api('/api/documents-with-tasks');
} catch(e) { console.error('load docs failed', e); }
document.getElementById('docCount').textContent = docs.length;
// ๆชๅฝ็ฑป็ๆๆกฃๆๅ้ข
docs.sort((a, b) => {
const aEmpty = (a.tasks || []).length === 0 ? 0 : 1;
const bEmpty = (b.tasks || []).length === 0 ? 0 : 1;
return aEmpty - bEmpty;
});
let html = '';
docs.forEach(d => {
const tasks = d.tasks || [];
const badgeHtml = tasks.length > 0
? tasks.map(t => `<span class="task-badge" title="${t.name}">๐ ${t.name}</span>`).join('')
: `<span class="task-badge task-badge-none">ๆชๅฝ็ฑป</span>`;
html += `<div class="doc-item" data-doc-id="${d.id}">`;
html += `<span class="doc-title" title="${d.title || d.id}">๐ ${d.title || d.id}</span>`;
html += badgeHtml;
html += `<span class="doc-actions">`;
html += `<span onclick="editDocTask('${d.id}','${(d.title || d.id).replace(/'/g, "\\'")}')" title="ไฟฎๆนๅฝๅฑtask">โ๏ธ ๅฝๅฑ</span>`;
html += `</span></div>`;
});
if (!html) html = '<div style="padding:20px;text-align:center;color:#555;">ๆๆ ๆๆกฃ</div>';
document.getElementById('docPanel').innerHTML = html;
}
function editDocTask(docId, docTitle) {
// ่ทๅๅฝๅๅทฒ้็ task
const docEl = document.querySelector(`.doc-item[data-doc-id="${docId}"]`);
let currentTaskId = null;
if (docEl) {
const badges = docEl.querySelectorAll('.task-badge');
for (const b of badges) {
currentTaskId = allTasksList.find(t => b.textContent.includes(t.name))?.id;
if (currentTaskId) break;
}
}
// ไฟๅญๅฐๅ
จๅฑๅ้ไพๅ่ฐไฝฟ็จ
window._editingDocId = docId;
window._editingDocTitle = docTitle;
TaskSelector.show({
title: `๐ ้ๆฉใ${docTitle}ใ็ๅฝๅฑ Task`,
currentTaskId: currentTaskId,
onSelect: async (task) => {
await api('/api/doc-tasks', 'POST', {
doc_id: docId,
task_ids: [task.id]
});
showToast(`โ
ๆๆกฃๅทฒๅฝๅฑใ${task.name}ใ`);
await renderDocList();
}
});
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ้็จ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function onSearchInput() {
renderAll();
renderTaskList();
}
function toggleDir(el) {
const dir = el.closest('.dir');
const dirId = dir.dataset.dirId;
dir.classList.toggle('open');
const nowOpen = dir.classList.contains('open');
el.querySelector('.toggle').textContent = nowOpen ? 'โผ' : 'โถ';
if (dirId) {
if (nowOpen) openDirIds.add(dirId);
else openDirIds.delete(dirId);
}
}
function selectTask(taskId, el) {
selectedTaskId = taskId;
document.querySelectorAll('.task-item').forEach(e => e.classList.remove('active'));
if (el) el.classList.add('active');
}
function matchesSearch(obj) {
const q = (document.getElementById('taskSearch').value || '').toLowerCase();
if (!q) return true;
return (obj.name || '').toLowerCase().includes(q);
}
// Modal
function showCreateTask() {
modalMode = 'create_task';
modalContext = {};
document.getElementById('modalTitle').textContent = 'ๆฐๅปบ Task';
document.getElementById('modalOverlay').style.display = 'block';
document.getElementById('modal').style.display = 'block';
document.getElementById('modalInput').style.display = '';
document.getElementById('modalInput').value = '';
document.getElementById('modalInput').placeholder = 'Task ๅ็งฐ';
document.getElementById('modalParent').style.display = 'none';
document.getElementById('modalInputContainer').innerHTML = `
<div class="doc-task-label">ๅฝๅฑ็ฎๅฝ๏ผๅฏ้๏ผ</div>
<select class="move-dir-select" id="taskDirSelect">${getDirOptionsHtml('')}</select>
`;
document.getElementById('modalConfirm').textContent = 'ๅๅปบ';
document.getElementById('modalConfirm').onclick = confirmModal;
document.getElementById('modalInput').focus();
}
function showCreateDir() { openModal('create', { parentId: 'td_root', parentName: 'ๆ น็ฎๅฝ' }, 'ๆฐๅปบ็ฎๅฝ'); }
function showCreateSubDir(parentId, parentName) { openModal('create', { parentId, parentName }, `ๅจใ${parentName}ใไธๆฐๅปบๅญ็ฎๅฝ`); }
function showRenameDir(dirId, name) { openModal('rename', { dirId, currentName: name }, '้ๅฝๅ็ฎๅฝ', name); }
function openModal(mode, ctx, title, val) {
modalMode = mode;
modalContext = ctx;
document.getElementById('modalTitle').textContent = title;
document.getElementById('modalOverlay').style.display = 'block';
document.getElementById('modal').style.display = 'block';
document.getElementById('modalInputContainer').innerHTML = '';
document.getElementById('modalInput').style.display = '';
document.getElementById('modalParent').style.display = '';
document.getElementById('modalInput').value = val || '';
document.getElementById('modalParent').textContent = ctx.parentName || '';
document.getElementById('modalConfirm').textContent = '็กฎๅฎ';
document.getElementById('modalConfirm').onclick = confirmModal;
document.getElementById('modalInput').focus();
}
function closeModal() {
document.getElementById('modalOverlay').style.display = 'none';
document.getElementById('modal').style.display = 'none';
document.getElementById('modalInputContainer').innerHTML = '';
document.getElementById('modalInput').style.display = '';
document.getElementById('modalParent').style.display = '';
document.getElementById('modalConfirm').textContent = '็กฎๅฎ';
document.getElementById('modalConfirm').onclick = confirmModal;
editingDocId = null;
}
async function confirmModal() {
const val = document.getElementById('modalInput').value.trim();
if (!val) return;
if (modalMode === 'create') {
const r = await api('/api/task-dirs', 'POST', {
action: 'create', name: val, parent_id: modalContext.parentId
});
if (r.ok) { showToast('โ
็ฎๅฝๅทฒๅๅปบ'); await loadData(); }
} else if (modalMode === 'create_task') {
const select = document.getElementById('taskDirSelect');
const dirId = select ? (select.value || null) : null;
const r = await api('/api/tasks/create', 'POST', { name: val, dir_id: dirId });
if (r.ok) { showToast('โ
Task ๅทฒๅๅปบ'); await loadData(); }
else showToast('โ ' + (r.error || 'ๅๅปบๅคฑ่ดฅ'));
} else if (modalMode === 'rename') {
const r = await api('/api/task-dirs', 'POST', {
action: 'rename', id: modalContext.dirId, name: val
});
if (r.ok) { showToast('โ
ๅทฒ้ๅฝๅ'); await loadData(); }
} else if (modalMode === 'rename_task') {
const r = await api('/api/tasks/rename', 'POST', {
task_id: modalContext.taskId, name: val
});
if (r.ok) { showToast('โ
Task ๅทฒ้ๅฝๅ'); await loadData(); }
else showToast('โ ' + (r.error || '้ๅฝๅๅคฑ่ดฅ'));
}
closeModal();
}
async function deleteDir(dirId, name) {
if (!confirm(`็กฎ่ฎคๅ ้คใ${name}ใ๏ผ`)) return;
const r = await api('/api/task-dirs', 'POST', { action: 'delete', id: dirId });
if (r.ok) { showToast('๐ ๅทฒๅ ้ค'); await loadData(); }
else showToast('โ ' + (r.error || 'ๅ ้คๅคฑ่ดฅ'));
}
function showRenameTask(taskId, name) {
openModal('rename_task', { taskId, currentName: name }, '้ๅฝๅ Task', name);
}
function showMoveTaskDir(taskId, taskName) {
const task = allTasks.find(t => t.id === taskId);
const currentDirId = task ? task.dir_id : null;
const dirPath = currentDirId ? getDirPath(currentDirId) : 'ๆชๅ็ฑป';
document.getElementById('modalTitle').textContent = `็งปๅจใ${taskName}ใๅฐ็ฎๅฝ`;
document.getElementById('modalOverlay').style.display = 'block';
document.getElementById('modal').style.display = 'block';
document.getElementById('modalInputContainer').innerHTML = `
<div class="doc-task-label">้ๆฉ็ฎๆ ็ฎๅฝ</div>
<div style="font-size:11px;color:#555;margin-bottom:10px;">ๅฝๅ: ${dirPath}</div>
<select class="move-dir-select" id="moveDirSelect">${getDirOptionsHtml(currentDirId)}</select>
`;
document.getElementById('modalInput').style.display = 'none';
document.getElementById('modalParent').style.display = 'none';
document.getElementById('modalConfirm').textContent = '็งปๅจ';
modalMode = 'move_task';
modalContext = { taskId, taskName };
document.getElementById('modalConfirm').onclick = confirmMoveTaskDir;
}
async function confirmMoveTaskDir() {
const select = document.getElementById('moveDirSelect');
if (!select) return;
const dirId = select.value || null;
const taskId = modalContext.taskId;
if (!taskId) return;
const r = await api('/api/task-dirs', 'POST', { action: 'move_task', task_id: taskId, dir_id: dirId });
if (r.ok) {
const dirName = dirId ? (allDirs.find(d => d.id === dirId)?.name || 'ๆ น็ฎๅฝ') : 'ๆชๅ็ฑป';
showToast(`โ
ๅทฒ็งปๅจๅฐใ${dirName}ใ`);
closeModal();
await loadData();
} else {
showToast('โ ' + (r.error || '็งปๅจๅคฑ่ดฅ'));
}
}
async function deleteTask(taskId, name) {
if (!confirm(`็กฎ่ฎคๅ ้คTaskใ${name}ใ๏ผ`)) return;
const r = await api('/api/tasks/delete', 'POST', { task_id: taskId });
if (r.ok) { showToast('๐ Task ๅทฒๅ ้ค'); selectedTaskId = null; await loadData(); }
else showToast('โ ' + (r.error || 'ๅ ้คๅคฑ่ดฅ'));
}
// โโ ๆๆฝ โโ
let draggedTaskId = null;
let draggedDirId = null;
function onTaskDragStart(event, taskId) {
draggedTaskId = taskId;
draggedDirId = null;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', 'task:' + taskId);
event.target.classList.add('dragging');
}
function onDirDragStart(event, dirId) {
event.stopPropagation();
draggedDirId = dirId;
draggedTaskId = null;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', 'dir:' + dirId);
event.target.closest('.dir-header').classList.add('dragging');
}
function onDirDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
event.currentTarget.classList.add('drag-over');
}
function onDirDragLeave(event) {
event.currentTarget.classList.remove('drag-over');
}
async function onDirDrop(event, dirId) {
event.preventDefault();
event.currentTarget.classList.remove('drag-over');
if (draggedTaskId) {
const task = allTasks.find(t => t.id === draggedTaskId);
if (!task) { draggedTaskId = null; return; }
if (task.dir_id === dirId) { draggedTaskId = null; showToast('โน๏ธ ๅทฒๅจๅไธ็ฎๅฝ'); return; }
const dir = allDirs.find(d => d.id === dirId);
await moveTask(draggedTaskId, dirId);
showToast(`โ
${task.name} โ ๅทฒ็งป่ณใ${dir ? dir.name : dirId}ใ`);
draggedTaskId = null;
document.querySelectorAll('.dragging').forEach(el => el.classList.remove('dragging'));
return;
}
if (draggedDirId) {
if (draggedDirId === dirId) { draggedDirId = null; showToast('โน๏ธ ไธ่ฝๆๅฐ่ชๅทฑไธ้ข'); return; }
function isDescendant(pid) {
const children = allDirs.filter(d => d.parent_id === pid);
return children.some(d => d.id === draggedDirId || isDescendant(d.id));
}
if (isDescendant(dirId)) {
draggedDirId = null; showToast('โ ไธ่ฝๆๅฐๅญ็ฎๅฝ'); return;
}
const srcDir = allDirs.find(d => d.id === draggedDirId);
const tgtDir = allDirs.find(d => d.id === dirId);
const r = await api('/api/task-dirs', 'POST', { action: 'move_dir', id: draggedDirId, parent_id: dirId });
if (r.ok) {
showToast(`โ
${srcDir ? srcDir.name : ''} โ ใ${tgtDir ? tgtDir.name : dirId}ใ`);
} else {
showToast('โ ' + (r.error || '็งปๅจๅคฑ่ดฅ'));
}
draggedDirId = null;
document.querySelectorAll('.dragging').forEach(el => el.classList.remove('dragging'));
await loadData();
}
}
async function moveTask(taskId, dirId) {
await api('/api/task-dirs', 'POST', { action: 'move_task', task_id: taskId, dir_id: dirId });
await loadData();
}
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg; t.style.display = 'block';
setTimeout(() => t.style.display = 'none', 2000);
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๆฝๅฑ้ข่ง
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let _markedLoaded = false;
function ensureMarked() {
if (typeof marked !== 'undefined') { _markedLoaded = true; return true; }
return false;
}
function stripFrontmatter(text) {
// ็งป้ค YAML frontmatter
return text.replace(/^---[\s\S]*?---\n*/, '').trim();
}
function openSkillPreview(name) {
const skill = allSkills.find(s => s.name === name);
if (!skill) { showToast('โ Skill ไธๅญๅจ'); return; }
document.getElementById('drawerTitle').textContent = 'โก ' + name;
document.getElementById('drawerContent').innerHTML = '<div class="drawer-loading">ๅ ่ฝฝไธญ...</div>';
document.getElementById('drawer').classList.add('open');
document.getElementById('drawerOverlay').style.display = 'block';
fetch('/api/skill-content?name=' + encodeURIComponent(name))
.then(r => r.json())
.then(data => {
if (data.error) {
document.getElementById('drawerContent').innerHTML = '<div style="color:#ef4444;padding:20px;">โ ' + data.error + '</div>';
return;
}
const desc = skill.description ? '<p style="color:#888;font-size:13px;margin-bottom:12px;">' + escapeHtml(skill.description) + '</p>' : '';
const tasks = skill.tasks && skill.tasks.length > 0
? '<div style="margin-bottom:12px;display:flex;flex-wrap:wrap;gap:4px;">' +
skill.tasks.map(t => '<span class="task-badge">๐ ' + escapeHtml(t.task_name) + '</span>').join('') +
'</div>'
: '';
const raw = stripFrontmatter(data.content);
let html;
if (ensureMarked()) {
// marked ๅ
จๅฑๆนๅผ
const markedLib = marked;
html = markedLib.parse(raw, { breaks: true, gfm: true });
} else {
html = '<pre style="white-space:pre-wrap;">' + escapeHtml(raw) + '</pre>';
}
document.getElementById('drawerContent').innerHTML = desc + tasks + '<hr>' + html;
})
.catch(e => {
document.getElementById('drawerContent').innerHTML = '<div style="color:#ef4444;padding:20px;">โ ๅ ่ฝฝๅคฑ่ดฅ: ' + e.message + '</div>';
});
}
function openDocPreview(docId, title) {
document.getElementById('drawerTitle').textContent = '๐ ' + title;
document.getElementById('drawerContent').innerHTML = '<div class="drawer-loading">ๅ ่ฝฝไธญ...</div>';
document.getElementById('drawer').classList.add('open');
document.getElementById('drawerOverlay').style.display = 'block';
fetch('/api/doc-content?doc_id=' + encodeURIComponent(docId))
.then(r => r.json())
.then(data => {
if (data.error || !data.content) {
document.getElementById('drawerContent').innerHTML = '<div style="color:#ef4444;padding:20px;">โ ' + (data.error || 'ๆๆกฃๅ
ๅฎนไธบ็ฉบ') + '</div>';
return;
}
const raw = stripFrontmatter(data.content);
let html;
if (ensureMarked()) {
const markedLib = marked;
html = markedLib.parse(raw, { breaks: true, gfm: true });
} else {
html = '<pre style="white-space:pre-wrap;">' + escapeHtml(raw) + '</pre>';
}
document.getElementById('drawerContent').innerHTML = html;
})
.catch(e => {
document.getElementById('drawerContent').innerHTML = '<div style="color:#ef4444;padding:20px;">โ ๅ ่ฝฝๅคฑ่ดฅ: ' + e.message + '</div>';
});
}
function closeDrawer() {
document.getElementById('drawer').classList.remove('open');
document.getElementById('drawerOverlay').style.display = 'none';
document.getElementById('drawerContent').innerHTML = '';
}
function escapeHtml(text) {
const d = document.createElement('div');
d.textContent = text;
return d.innerHTML;
}
// ็ป skill ๅ็งฐๅๆๆกฃๆ ้ขๆทปๅ ็นๅป้ข่ง
document.addEventListener('click', function(e) {
// Skill ๅ็งฐ็นๅป
const skillNameEl = e.target.closest('.skill-name');
if (skillNameEl) {
const name = skillNameEl.textContent.replace('โก ', '').trim();
if (name) openSkillPreview(name);
return;
}
// ๆๆกฃๆ ้ข็นๅป
const docTitleEl = e.target.closest('.doc-title');
if (docTitleEl) {
const docItem = docTitleEl.closest('.doc-item');
if (docItem) {
const docId = docItem.dataset.docId;
const title = docTitleEl.textContent.replace('๐ ', '').trim();
if (docId) openDocPreview(docId, title);
}
return;
}
});
// โโ ๅๅงๅ่ชๅจๅฑๅผ โโ
openSkillCat.add('ๆชๅ็ฑป');
openSkillCat.add('utilities');
openSkillCat.add('infrastructure');
openSkillCat.add('core');
openSkillCat.add('communication');
openSkillCat.add('finance');
openSkillCat.add('frontend');
init();
</script>
</body>
</html>