Open-source AI data analyst platform extracted from internal repo. Includes data sync engine, Keboola adapter, Flask web portal, server deployment scripts, and configuration templates.
815 lines
30 KiB
HTML
815 lines
30 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>
|
|
<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">
|
|
<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;
|
|
}
|
|
|
|
/* 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>
|
|
</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 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>
|
|
</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>
|