Admin page at /corporate-memory/admin with three tabs: - Review Queue: pending items with approve/mandate/reject + batch ops - All Items: status filter, promote/demote/revoke actions - Audit Log: filterable action history table Features: - Keyboard shortcuts (j/k navigate, a/r/m = approve/reject/mandate) - Inline mandate form (mandatory reason + audience targeting) - Toast notifications on action success/error - Pending count badge on main Corporate Memory page - Matches existing visual design (CSS variables, card styles)
865 lines
32 KiB
HTML
865 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Corporate Memory - Data Analyst Portal</title>
|
|
{% if not config.THEME_FONT_URL %}
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
{% endif %}
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='style-custom.css') }}">
|
|
<style>
|
|
/* Corporate Memory Page - Design System Styles */
|
|
.container-memory {
|
|
max-width: 1000px;
|
|
margin: 0 auto;
|
|
padding: var(--space-6);
|
|
}
|
|
|
|
/* Header */
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding-bottom: var(--space-5);
|
|
border-bottom: 1px solid var(--border);
|
|
margin-bottom: var(--space-6);
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-4);
|
|
}
|
|
|
|
.back-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
font-size: var(--text-sm);
|
|
padding: var(--space-2) var(--space-3);
|
|
border-radius: var(--radius-md);
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.back-link:hover {
|
|
background: var(--border-light);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.page-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-3);
|
|
}
|
|
|
|
.page-title h1 {
|
|
font-size: var(--text-xl);
|
|
font-weight: var(--font-semibold);
|
|
}
|
|
|
|
.page-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
background: var(--warning);
|
|
border-radius: var(--radius-lg);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
}
|
|
|
|
/* Stats Bar */
|
|
.stats-bar {
|
|
display: flex;
|
|
gap: var(--space-6);
|
|
padding: var(--space-5);
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-xl);
|
|
box-shadow: var(--shadow-sm);
|
|
margin-bottom: var(--space-6);
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-item .value {
|
|
font-size: var(--text-xl);
|
|
font-weight: var(--font-bold);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.stat-item .label {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.stat-item.highlight .value {
|
|
color: var(--warning);
|
|
}
|
|
|
|
.stat-divider {
|
|
width: 1px;
|
|
background: var(--border);
|
|
}
|
|
|
|
.stats-bar-right {
|
|
margin-left: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-3);
|
|
}
|
|
|
|
.sync-info {
|
|
font-size: var(--text-sm);
|
|
color: var(--success);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
/* Toolbar */
|
|
.toolbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--space-5);
|
|
gap: var(--space-4);
|
|
}
|
|
|
|
.search-box {
|
|
flex: 1;
|
|
max-width: 320px;
|
|
position: relative;
|
|
}
|
|
|
|
.search-box input {
|
|
width: 100%;
|
|
padding: var(--space-2) var(--space-3) var(--space-2) 36px;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
font-size: var(--text-base);
|
|
background: var(--surface);
|
|
}
|
|
|
|
.search-box input:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 2px var(--primary-light);
|
|
}
|
|
|
|
.search-box svg {
|
|
position: absolute;
|
|
left: var(--space-3);
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.filter-group {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.filter-btn {
|
|
padding: var(--space-2) var(--space-3);
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
font-size: var(--text-sm);
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.filter-btn:hover {
|
|
border-color: var(--text-secondary);
|
|
}
|
|
|
|
.filter-btn.active {
|
|
background: var(--primary-light);
|
|
border-color: var(--primary);
|
|
color: var(--primary);
|
|
}
|
|
|
|
.sort-select {
|
|
padding: var(--space-2) var(--space-3);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
font-size: var(--text-sm);
|
|
background: var(--surface);
|
|
color: var(--text-primary);
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Knowledge List */
|
|
.knowledge-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-4);
|
|
}
|
|
|
|
.knowledge-item {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-xl);
|
|
box-shadow: var(--shadow-sm);
|
|
padding: var(--space-5);
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.knowledge-item:hover {
|
|
border-color: var(--primary);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.knowledge-item.synced {
|
|
border-left: 3px solid var(--success);
|
|
}
|
|
|
|
.knowledge-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: var(--space-3);
|
|
}
|
|
|
|
.knowledge-title {
|
|
font-size: var(--text-md);
|
|
font-weight: var(--font-semibold);
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--space-1);
|
|
}
|
|
|
|
.knowledge-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-3);
|
|
font-size: var(--text-xs);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.category-badge {
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-full);
|
|
font-size: var(--text-xs);
|
|
font-weight: var(--font-medium);
|
|
}
|
|
|
|
.category-badge.data_analysis { background: rgba(16, 183, 127, 0.1); color: var(--success); }
|
|
.category-badge.api_integration { background: var(--primary-light); color: var(--primary); }
|
|
.category-badge.debugging { background: rgba(234, 88, 12, 0.1); color: var(--error); }
|
|
.category-badge.performance { background: rgba(245, 159, 10, 0.1); color: #b45309; }
|
|
.category-badge.workflow { background: rgba(139, 92, 246, 0.1); color: #7c3aed; }
|
|
.category-badge.infrastructure { background: rgba(107, 114, 128, 0.1); color: var(--text-secondary); }
|
|
.category-badge.business_logic { background: rgba(16, 183, 127, 0.1); color: #0d9668; }
|
|
|
|
.knowledge-content {
|
|
font-size: var(--text-base);
|
|
color: var(--text-secondary);
|
|
line-height: 1.7;
|
|
margin-bottom: var(--space-4);
|
|
}
|
|
|
|
.knowledge-content code {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-sm);
|
|
background: var(--background);
|
|
padding: 2px 6px;
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.knowledge-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding-top: var(--space-3);
|
|
border-top: 1px solid var(--border-light);
|
|
}
|
|
|
|
.knowledge-tags {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.tag {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-secondary);
|
|
background: var(--background);
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
.contributors {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-secondary);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.contributor-avatars {
|
|
display: flex;
|
|
}
|
|
|
|
.contributor-avatar {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: var(--radius-full);
|
|
background: var(--primary-light);
|
|
border: 2px solid var(--surface);
|
|
margin-left: -6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 8px;
|
|
font-weight: var(--font-semibold);
|
|
color: var(--primary);
|
|
}
|
|
|
|
.contributor-avatar:first-child {
|
|
margin-left: 0;
|
|
}
|
|
|
|
/* Voting */
|
|
.vote-section {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-4);
|
|
}
|
|
|
|
.vote-buttons {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.vote-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-1);
|
|
padding: var(--space-2) var(--space-3);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
background: var(--surface);
|
|
font-size: var(--text-sm);
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.vote-btn:hover {
|
|
border-color: var(--text-secondary);
|
|
}
|
|
|
|
.vote-btn.upvote.active {
|
|
background: rgba(16, 183, 127, 0.1);
|
|
border-color: var(--success);
|
|
color: var(--success);
|
|
}
|
|
|
|
.vote-btn.downvote.active {
|
|
background: rgba(234, 88, 12, 0.1);
|
|
border-color: var(--error);
|
|
color: var(--error);
|
|
}
|
|
|
|
.vote-btn svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.synced-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-1);
|
|
font-size: var(--text-xs);
|
|
color: var(--success);
|
|
background: rgba(16, 183, 127, 0.1);
|
|
padding: var(--space-1) var(--space-2);
|
|
border-radius: var(--radius-full);
|
|
}
|
|
|
|
/* Pagination */
|
|
.pagination {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: var(--space-2);
|
|
margin-top: var(--space-6);
|
|
}
|
|
|
|
.page-btn {
|
|
padding: var(--space-2) var(--space-3);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
background: var(--surface);
|
|
font-size: var(--text-sm);
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.page-btn:hover:not(:disabled) {
|
|
border-color: var(--primary);
|
|
color: var(--primary);
|
|
}
|
|
|
|
.page-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.page-btn.active {
|
|
background: var(--primary);
|
|
border-color: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
/* Empty State */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: var(--space-8);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state svg {
|
|
width: 48px;
|
|
height: 48px;
|
|
margin-bottom: var(--space-4);
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* Admin Link Button */
|
|
.admin-link-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
padding: var(--space-2) var(--space-3);
|
|
background: var(--surface);
|
|
border: 1px solid var(--primary);
|
|
border-radius: var(--radius-md);
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
font-size: var(--text-sm);
|
|
font-weight: var(--font-medium);
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.admin-link-btn:hover {
|
|
background: var(--primary-light);
|
|
}
|
|
|
|
.pending-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 18px;
|
|
height: 18px;
|
|
padding: 0 5px;
|
|
border-radius: var(--radius-full);
|
|
background: var(--warning);
|
|
color: white;
|
|
font-size: 10px;
|
|
font-weight: var(--font-bold);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.page-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: var(--space-3);
|
|
}
|
|
|
|
.stats-bar {
|
|
flex-wrap: wrap;
|
|
gap: var(--space-4);
|
|
}
|
|
|
|
.toolbar {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.search-box {
|
|
max-width: none;
|
|
}
|
|
|
|
.filter-group {
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.knowledge-header {
|
|
flex-direction: column;
|
|
gap: var(--space-3);
|
|
}
|
|
|
|
.knowledge-footer {
|
|
flex-direction: column;
|
|
gap: var(--space-3);
|
|
align-items: flex-start;
|
|
}
|
|
}
|
|
</style>
|
|
{% include '_theme.html' %}
|
|
</head>
|
|
<body>
|
|
<div class="container-memory">
|
|
<!-- Header -->
|
|
<header class="page-header">
|
|
<div class="header-left">
|
|
<a href="{{ url_for('dashboard') }}" class="back-link">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M15 18l-6-6 6-6"/>
|
|
</svg>
|
|
Dashboard
|
|
</a>
|
|
<div class="page-title">
|
|
<span class="page-icon">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
|
<path d="M2 17l10 5 10-5"/>
|
|
<path d="M2 12l10 5 10-5"/>
|
|
</svg>
|
|
</span>
|
|
<h1>Corporate Memory</h1>
|
|
</div>
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: var(--space-4);">
|
|
{% if governance.is_km_admin %}
|
|
<a href="{{ url_for('corporate_memory_admin') }}" class="admin-link-btn">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
|
</svg>
|
|
Admin Review
|
|
{% if governance.pending_count > 0 %}
|
|
<span class="pending-badge">{{ governance.pending_count }}</span>
|
|
{% endif %}
|
|
</a>
|
|
{% endif %}
|
|
<div class="user-info-v2">
|
|
{% if session.user.picture %}
|
|
<img src="{{ session.user.picture }}" alt="Profile" class="avatar-v2">
|
|
{% else %}
|
|
<div class="avatar-v2" style="background: var(--primary-light); display: flex; align-items: center; justify-content: center; font-weight: 600; color: var(--primary); font-size: 12px;">
|
|
{{ session.user.email[:2].upper() }}
|
|
</div>
|
|
{% endif %}
|
|
{{ session.user.email }}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Stats Bar -->
|
|
<div class="stats-bar">
|
|
<div class="stat-item">
|
|
<div class="value">{{ stats.contributors }}</div>
|
|
<div class="label">Contributors</div>
|
|
</div>
|
|
<div class="stat-divider"></div>
|
|
<div class="stat-item">
|
|
<div class="value">{{ stats.knowledge_count }}</div>
|
|
<div class="label">Knowledge Items</div>
|
|
</div>
|
|
<div class="stat-divider"></div>
|
|
<div class="stat-item highlight">
|
|
<div class="value">{{ user_stats.your_rules }}</div>
|
|
<div class="label">Your Rules</div>
|
|
</div>
|
|
<div class="stats-bar-right">
|
|
<div class="sync-info">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="20 6 9 17 4 12"/>
|
|
</svg>
|
|
Synced to .claude/rules/
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toolbar -->
|
|
<div class="toolbar">
|
|
<div class="search-box">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="11" cy="11" r="8"/>
|
|
<path d="M21 21l-4.35-4.35"/>
|
|
</svg>
|
|
<input type="text" id="searchInput" placeholder="Search knowledge..." onkeyup="debounceSearch()">
|
|
</div>
|
|
<div class="filter-group">
|
|
<button class="filter-btn active" data-category="" onclick="setFilter(this)">All</button>
|
|
<button class="filter-btn" data-category="my_rules" onclick="setFilter(this)">My Rules</button>
|
|
<button class="filter-btn" data-category="data_analysis" onclick="setFilter(this)">Data Analysis</button>
|
|
<button class="filter-btn" data-category="api_integration" onclick="setFilter(this)">API</button>
|
|
<button class="filter-btn" data-category="performance" onclick="setFilter(this)">Performance</button>
|
|
</div>
|
|
<select class="sort-select" id="sortSelect" onchange="applyFilters()">
|
|
<option value="score">Most Popular</option>
|
|
<option value="updated_at">Most Recent</option>
|
|
<option value="contributors">Most Contributors</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Knowledge List -->
|
|
<div id="knowledgeList" class="knowledge-list">
|
|
{% for item in knowledge['items'] %}
|
|
<div class="knowledge-item {% if item.synced %}synced{% endif %}" data-id="{{ item.id }}">
|
|
<div class="knowledge-header">
|
|
<div>
|
|
<div class="knowledge-title">{{ item.title }}</div>
|
|
<div class="knowledge-meta">
|
|
<span class="category-badge {{ item.category }}">{{ item.category.replace('_', ' ').title() }}</span>
|
|
<span>Added {{ item.extracted_at[:10] if item.extracted_at else 'recently' }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="vote-section">
|
|
<div class="vote-buttons">
|
|
<button class="vote-btn upvote {% if user_votes.get(item.id, 0) == 1 %}active{% endif %}"
|
|
onclick="vote('{{ item.id }}', 1)" title="Upvote">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14z"/>
|
|
<path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/>
|
|
</svg>
|
|
<span>{{ item.upvotes or 0 }}</span>
|
|
</button>
|
|
<button class="vote-btn downvote {% if user_votes.get(item.id, 0) == -1 %}active{% endif %}"
|
|
onclick="vote('{{ item.id }}', -1)" title="Downvote">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10z"/>
|
|
<path d="M17 2h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"/>
|
|
</svg>
|
|
<span>{{ item.downvotes or 0 }}</span>
|
|
</button>
|
|
</div>
|
|
{% if item.synced %}
|
|
<span class="synced-badge">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="20 6 9 17 4 12"/>
|
|
</svg>
|
|
In your rules
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="knowledge-content">{{ item.content }}</div>
|
|
<div class="knowledge-footer">
|
|
<div class="knowledge-tags">
|
|
{% for tag in item.tags %}
|
|
<span class="tag">{{ tag }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="contributors">
|
|
<div class="contributor-avatars">
|
|
{% for user_info in item.source_users_display[:3] %}
|
|
<span class="contributor-avatar" title="{{ user_info.name }}">{{ user_info.initials }}</span>
|
|
{% endfor %}
|
|
{% if item.source_users_display|length > 3 %}
|
|
<span class="contributor-avatar">+{{ item.source_users_display|length - 3 }}</span>
|
|
{% endif %}
|
|
</div>
|
|
<span>{{ item.source_users_display|length }} contributor{{ 's' if item.source_users_display|length != 1 else '' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
|
|
</svg>
|
|
<p>No knowledge items yet.</p>
|
|
<p style="font-size: var(--text-sm);">Knowledge is extracted from team CLAUDE.local.md files every 30 minutes.</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div id="pagination" class="pagination">
|
|
{% if knowledge.total_pages > 1 %}
|
|
<button class="page-btn" onclick="goToPage({{ knowledge.page - 1 }})" {% if knowledge.page == 0 %}disabled{% endif %}>Prev</button>
|
|
{% for p in range(knowledge.total_pages) %}
|
|
<button class="page-btn {% if p == knowledge.page %}active{% endif %}" onclick="goToPage({{ p }})">{{ p + 1 }}</button>
|
|
{% endfor %}
|
|
<button class="page-btn" onclick="goToPage({{ knowledge.page + 1 }})" {% if knowledge.page >= knowledge.total_pages - 1 %}disabled{% endif %}>Next</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Store user votes locally for UI updates
|
|
let userVotes = {{ user_votes | tojson }};
|
|
let currentPage = {{ knowledge.page }};
|
|
let currentCategory = '';
|
|
let searchTimeout = null;
|
|
|
|
function debounceSearch() {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(applyFilters, 300);
|
|
}
|
|
|
|
function setFilter(btn) {
|
|
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
currentCategory = btn.dataset.category;
|
|
currentPage = 0;
|
|
applyFilters();
|
|
}
|
|
|
|
async function applyFilters() {
|
|
const search = document.getElementById('searchInput').value;
|
|
const sort = document.getElementById('sortSelect').value;
|
|
await loadKnowledge(currentCategory, search, currentPage, sort);
|
|
}
|
|
|
|
async function goToPage(page) {
|
|
currentPage = page;
|
|
await applyFilters();
|
|
}
|
|
|
|
async function loadKnowledge(category, search, page, sort) {
|
|
const params = new URLSearchParams();
|
|
if (category && category !== 'my_rules') params.append('category', category);
|
|
if (category === 'my_rules') params.append('my_rules', 'true');
|
|
if (search) params.append('search', search);
|
|
if (sort) params.append('sort', sort);
|
|
params.append('page', page);
|
|
|
|
try {
|
|
params.append('_t', Date.now());
|
|
const resp = await fetch(`/api/corporate-memory/knowledge?${params}`);
|
|
const data = await resp.json();
|
|
renderKnowledge(data);
|
|
} catch (e) {
|
|
console.error('Failed to load knowledge:', e);
|
|
}
|
|
}
|
|
|
|
function renderKnowledge(data) {
|
|
const list = document.getElementById('knowledgeList');
|
|
const pagination = document.getElementById('pagination');
|
|
|
|
if (data.items.length === 0) {
|
|
list.innerHTML = `
|
|
<div class="empty-state">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
|
|
</svg>
|
|
<p>No matching knowledge items found.</p>
|
|
</div>`;
|
|
pagination.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = data.items.map(item => `
|
|
<div class="knowledge-item ${item.synced ? 'synced' : ''}" data-id="${item.id}">
|
|
<div class="knowledge-header">
|
|
<div>
|
|
<div class="knowledge-title">${escapeHtml(item.title)}</div>
|
|
<div class="knowledge-meta">
|
|
<span class="category-badge ${item.category}">${escapeHtml(item.category.replace('_', ' '))}</span>
|
|
<span>Added ${item.extracted_at ? item.extracted_at.substring(0, 10) : 'recently'}</span>
|
|
</div>
|
|
</div>
|
|
<div class="vote-section">
|
|
<div class="vote-buttons">
|
|
<button class="vote-btn upvote ${userVotes[item.id] === 1 ? 'active' : ''}"
|
|
onclick="vote('${item.id}', 1)" title="Upvote">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14z"/>
|
|
<path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/>
|
|
</svg>
|
|
<span>${item.upvotes || 0}</span>
|
|
</button>
|
|
<button class="vote-btn downvote ${userVotes[item.id] === -1 ? 'active' : ''}"
|
|
onclick="vote('${item.id}', -1)" title="Downvote">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10z"/>
|
|
<path d="M17 2h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"/>
|
|
</svg>
|
|
<span>${item.downvotes || 0}</span>
|
|
</button>
|
|
</div>
|
|
${item.synced ? `
|
|
<span class="synced-badge">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="20 6 9 17 4 12"/>
|
|
</svg>
|
|
In your rules
|
|
</span>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="knowledge-content">${escapeHtml(item.content)}</div>
|
|
<div class="knowledge-footer">
|
|
<div class="knowledge-tags">
|
|
${(item.tags || []).map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join('')}
|
|
</div>
|
|
<div class="contributors">
|
|
<div class="contributor-avatars">
|
|
${(item.source_users_display || []).slice(0, 3).map(u =>
|
|
`<span class="contributor-avatar" title="${escapeHtml(u.name)}">${escapeHtml(u.initials)}</span>`
|
|
).join('')}
|
|
${(item.source_users_display || []).length > 3 ?
|
|
`<span class="contributor-avatar">+${item.source_users_display.length - 3}</span>` : ''}
|
|
</div>
|
|
<span>${(item.source_users_display || []).length} contributor${item.source_users_display?.length !== 1 ? 's' : ''}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Render pagination
|
|
if (data.total_pages > 1) {
|
|
let paginationHtml = `
|
|
<button class="page-btn" onclick="goToPage(${data.page - 1})" ${data.page === 0 ? 'disabled' : ''}>Prev</button>
|
|
`;
|
|
for (let p = 0; p < data.total_pages; p++) {
|
|
paginationHtml += `<button class="page-btn ${p === data.page ? 'active' : ''}" onclick="goToPage(${p})">${p + 1}</button>`;
|
|
}
|
|
paginationHtml += `
|
|
<button class="page-btn" onclick="goToPage(${data.page + 1})" ${data.page >= data.total_pages - 1 ? 'disabled' : ''}>Next</button>
|
|
`;
|
|
pagination.innerHTML = paginationHtml;
|
|
} else {
|
|
pagination.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
async function vote(itemId, voteValue) {
|
|
// Toggle vote if clicking same button
|
|
const currentVote = userVotes[itemId] || 0;
|
|
const newVote = currentVote === voteValue ? 0 : voteValue;
|
|
|
|
try {
|
|
const resp = await fetch('/api/corporate-memory/vote', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({item_id: itemId, vote: newVote})
|
|
});
|
|
|
|
if (resp.ok) {
|
|
userVotes[itemId] = newVote;
|
|
// Reload to get updated scores
|
|
applyFilters();
|
|
} else {
|
|
const err = await resp.json().catch(() => ({}));
|
|
console.error('Vote failed:', resp.status, err);
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to vote:', e);
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|