|
{% extends "base.html" %}
|
|
{% block extra_head %}
|
|
<link rel="stylesheet" href="{{ static_url('/static/css/feed.css') }}">
|
|
<link rel="stylesheet" href="{{ static_url('/static/css/profile.css') }}">
|
|
<link rel="stylesheet" href="{{ static_url('/static/css/projects.css') }}">
|
|
<link rel="stylesheet" href="{{ static_url('/static/css/gists.css') }}">
|
|
<link rel="stylesheet" href="{{ static_url('/static/css/awards.css') }}">
|
|
{% endblock %}
|
|
{% block content %}
|
|
<div class="profile-layout">
|
|
<aside class="profile-sidebar">
|
|
<div class="profile-card">
|
|
<div class="profile-avatar-wrap">
|
|
<span class="avatar-badge"><img src="{{ avatar_url('multiavatar', avatar_seed(profile_user), 80) }}" class="avatar-img avatar-lg" alt="{{ profile_user['username'] }}" id="profile-avatar-preview" loading="lazy">{% set _user = profile_user %}{% include "_presence_dot.html" %}{% include "_award_badge.html" %}</span>
|
|
{% if is_owner or viewer_is_admin %}
|
|
<button type="button" class="btn btn-sm avatar-regen-btn" data-regenerate-avatar data-username="{{ profile_user['username'] }}">Regenerate avatar</button>
|
|
{% endif %}
|
|
</div>
|
|
<h1 class="profile-name">{{ profile_user['username'] }}</h1>
|
|
|
|
<div class="profile-presence{% if profile_online %} online{% endif %}" role="status" data-presence-uid="{{ profile_user['uid'] }}" data-presence-last-seen="{{ profile_user.get('last_seen') or '' }}" data-presence-label>{% if profile_online %}online{% elif profile_user.get('last_seen') %}last seen {{ format_date(profile_user['last_seen']) }}{% else %}offline{% endif %}</div>
|
|
|
|
<div class="profile-stats">
|
|
<div class="profile-stat">
|
|
<span class="profile-stat-value">{{ posts_count }}</span>
|
|
<span class="profile-stat-label">Posts</span>
|
|
</div>
|
|
<div class="profile-stat">
|
|
<span class="profile-stat-value">{{ profile_user.get('level', 1) }}</span>
|
|
<span class="profile-stat-label">Level</span>
|
|
</div>
|
|
<div class="profile-stat">
|
|
<span class="profile-stat-value">{{ profile_user.get('stars', 0) }}</span>
|
|
<span class="profile-stat-label">Stars</span>
|
|
</div>
|
|
<div class="profile-stat">
|
|
<a href="/leaderboard" class="profile-stat-value" aria-label="Rank{% if rank %} number {{ rank }}{% else %} not ranked{% endif %}, view leaderboard">{% if rank %}#{{ rank }}{% else %}-{% endif %}</a>
|
|
<span class="profile-stat-label">Rank</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="profile-level-bar">
|
|
<div class="level-label">
|
|
<span>Progress to next level</span>
|
|
<span>{{ (profile_user.get('xp') or 0) % 100 }}%</span>
|
|
</div>
|
|
<div class="bar">
|
|
<div class="bar-fill" style="--bar-pct: {{ (profile_user.get('xp') or 0) % 100 }}%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="profile-badges">
|
|
{% for badge in badges %}
|
|
{% set meta = badge_info(badge['badge_name']) %}
|
|
<span class="profile-badge" title="{{ meta['description'] }}"><span class="profile-badge-icon">{{ meta['icon'] }}</span>{{ badge['badge_name'] }}</span>
|
|
{% else %}
|
|
{% set meta = badge_info('Member') %}
|
|
<span class="profile-badge" title="{{ meta['description'] }}"><span class="profile-badge-icon">{{ meta['icon'] }}</span>Member</span>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if not is_self(user, profile_user['uid']) %}
|
|
<div class="profile-actions">
|
|
<a href="{% if user %}/messages?with_uid={{ profile_user['uid'] }}{% else %}/auth/login{% endif %}" class="btn btn-secondary btn-sm full-width justify-center{% if not user %} login-required{% endif %}"><span class="icon">✉️</span>Send Message</a>
|
|
{% if is_following %}
|
|
<form method="POST" action="/follow/unfollow/{{ profile_user['username'] }}">
|
|
<button type="submit" class="btn btn-secondary btn-sm full-width"{{ guest_disabled(user) }}><span class="icon">➖</span>Unfollow</button>
|
|
</form>
|
|
{% else %}
|
|
<form method="POST" action="/follow/{{ profile_user['username'] }}">
|
|
<button type="submit" class="btn btn-primary btn-sm full-width"{{ guest_disabled(user) }}><span class="icon">➕</span>Follow</button>
|
|
</form>
|
|
{% endif %}
|
|
{% if is_blocked %}
|
|
<form method="POST" action="/block/unblock/{{ profile_user['username'] }}">
|
|
<button type="submit" class="btn btn-secondary btn-sm full-width" data-confirm="Unblock {{ profile_user['username'] }}? Their posts, comments and messages will be visible to you again."{{ guest_disabled(user) }}><span class="icon">🔓</span>Unblock</button>
|
|
</form>
|
|
{% else %}
|
|
<form method="POST" action="/block/{{ profile_user['username'] }}">
|
|
<button type="submit" class="btn btn-secondary btn-sm full-width" data-confirm="Block {{ profile_user['username'] }}? You will no longer see their posts, comments or messages anywhere except on this profile." data-confirm-danger{{ guest_disabled(user) }}><span class="icon">🚫</span>Block</button>
|
|
</form>
|
|
{% endif %}
|
|
{% if is_muted %}
|
|
<form method="POST" action="/mute/unmute/{{ profile_user['username'] }}">
|
|
<button type="submit" class="btn btn-secondary btn-sm full-width" data-confirm="Unmute {{ profile_user['username'] }}? They will be able to create notifications for you again."{{ guest_disabled(user) }}><span class="icon">🔊</span>Unmute</button>
|
|
</form>
|
|
{% else %}
|
|
<form method="POST" action="/mute/{{ profile_user['username'] }}">
|
|
<button type="submit" class="btn btn-secondary btn-sm full-width" data-confirm="Mute {{ profile_user['username'] }}? They will no longer create notifications for you." data-confirm-danger{{ guest_disabled(user) }}><span class="icon">🔇</span>Mute</button>
|
|
</form>
|
|
{% endif %}
|
|
{% if can_give_award %}
|
|
<button type="button" class="btn btn-primary btn-sm full-width" data-award-give data-username="{{ profile_user['username'] }}" data-modal="award-give-modal"{{ guest_disabled(user) }}><span class="icon">🏆</span>Give Award</button>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if is_self(user, profile_user['uid']) %}
|
|
<div class="profile-info">
|
|
<form method="POST" action="/profile/update">
|
|
<div class="profile-info-row">
|
|
<span class="profile-info-label">Bio</span>
|
|
<div class="flex-center-gap">
|
|
<div class="profile-info-value bio-display rendered-content" id="display-bio">{{ render_content(profile_user.get('bio', ''), author_is_admin=is_admin(profile_user)) or 'Not set' }}</div>
|
|
<button type="button" class="profile-info-edit" data-edit-field="bio" aria-label="Edit bio">✎</button>
|
|
</div>
|
|
</div>
|
|
<div class="auth-field hidden input-edit-field" id="input-bio">
|
|
<textarea name="bio" maxlength="500" rows="2" class="input-sm" aria-label="Bio">{{ profile_user.get('bio', '') }}</textarea>
|
|
</div>
|
|
|
|
<div class="profile-info-row">
|
|
<span class="profile-info-label">Location</span>
|
|
<div class="flex-center-gap">
|
|
<span class="profile-info-value" id="display-location">{{ profile_user.get('location', '') or 'Not set' }}</span>
|
|
<button type="button" class="profile-info-edit" data-edit-field="location" aria-label="Edit location">✎</button>
|
|
</div>
|
|
</div>
|
|
<div class="auth-field hidden input-edit-field" id="input-location">
|
|
<input type="text" name="location" maxlength="200" value="{{ profile_user.get('location', '') }}" class="input-sm" aria-label="Location">
|
|
</div>
|
|
|
|
<div class="profile-info-row">
|
|
<span class="profile-info-label">Git Link</span>
|
|
<div class="flex-center-gap">
|
|
<span class="profile-info-value" id="display-git_link">{{ profile_user.get('git_link', '') or 'Not set' }}</span>
|
|
<button type="button" class="profile-info-edit" data-edit-field="git_link" aria-label="Edit Git link">✎</button>
|
|
</div>
|
|
</div>
|
|
<div class="auth-field hidden input-edit-field" id="input-git_link">
|
|
<input type="url" name="git_link" maxlength="500" value="{{ profile_user.get('git_link', '') }}" class="input-sm" aria-label="Git link">
|
|
</div>
|
|
|
|
<div class="profile-info-row">
|
|
<span class="profile-info-label">Website</span>
|
|
<div class="flex-center-gap">
|
|
<span class="profile-info-value" id="display-website">{{ profile_user.get('website', '') or 'Not set' }}</span>
|
|
<button type="button" class="profile-info-edit" data-edit-field="website" aria-label="Edit website">✎</button>
|
|
</div>
|
|
</div>
|
|
<div class="auth-field hidden input-edit-field" id="input-website">
|
|
<input type="url" name="website" maxlength="500" value="{{ profile_user.get('website', '') }}" class="input-sm" aria-label="Website">
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary btn-sm full-width save-btn-top">Save Changes</button>
|
|
</form>
|
|
</div>
|
|
{% else %}
|
|
<div class="profile-info">
|
|
<div class="profile-info-row">
|
|
<span class="profile-info-label">Bio</span>
|
|
<div class="profile-info-value rendered-content">{{ render_content(profile_user.get('bio', ''), author_is_admin=is_admin(profile_user)) or 'Not set' }}</div>
|
|
</div>
|
|
<div class="profile-info-row">
|
|
<span class="profile-info-label">Location</span>
|
|
<span class="profile-info-value">{{ profile_user.get('location', '') or 'Not set' }}</span>
|
|
</div>
|
|
<div class="profile-info-row">
|
|
<span class="profile-info-label">Git Link</span>
|
|
<span class="profile-info-value">{{ profile_user.get('git_link', '') or 'Not set' }}</span>
|
|
</div>
|
|
<div class="profile-info-row">
|
|
<span class="profile-info-label">Website</span>
|
|
<span class="profile-info-value">{{ profile_user.get('website', '') or 'Not set' }}</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if can_view_api_key %}
|
|
<div class="api-key-card" data-api-key-card>
|
|
<div class="api-key-head">
|
|
<span class="api-key-title">API Key</span>
|
|
<a href="/docs/authentication.html" class="api-key-docs">Docs</a>
|
|
</div>
|
|
<code class="api-key-value" data-api-key data-key="{{ api_key }}" data-masked="true">{{ '•' * 12 }}</code>
|
|
<div class="api-key-actions">
|
|
<button type="button" class="btn btn-sm" data-api-key-reveal>Reveal</button>
|
|
<button type="button" class="btn btn-sm" data-api-key-copy>Copy</button>
|
|
{% if is_owner %}
|
|
<button type="button" class="btn btn-sm btn-primary" data-regen-api-key>Regenerate</button>
|
|
{% endif %}
|
|
</div>
|
|
<p class="api-key-hint">Use this key with <code>X-API-KEY</code>, <code>Authorization: Bearer</code>, or HTTP Basic auth on any request. {% if is_owner %}Regenerating immediately invalidates the old key.{% endif %}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if can_manage_customization %}
|
|
<div class="customization-card" data-customization data-username="{{ profile_user['username'] }}">
|
|
<div class="customization-head">
|
|
<span class="customization-title">{% if is_owner %}My customizations{% else %}{{ profile_user['username'] }}'s customizations{% endif %}</span>
|
|
</div>
|
|
<div class="customization-row">
|
|
<span class="customization-label">Site-wide custom CSS and JS</span>
|
|
<form method="POST" action="/profile/{{ profile_user['username'] }}/customization/global" data-customization-form>
|
|
<input type="hidden" name="value" value="{{ 1 if cust_disable_global else 0 }}">
|
|
<button type="submit" class="btn btn-sm {% if cust_disable_global %}btn-primary{% endif %}" data-customization-toggle="global" data-confirm="{% if cust_disable_global %}Re-enable your site-wide custom CSS and JS?{% else %}Disable your site-wide custom CSS and JS? Your saved code is kept and can be re-enabled.{% endif %}">{% if cust_disable_global %}Enable{% else %}Disable{% endif %}</button>
|
|
</form>
|
|
</div>
|
|
<div class="customization-row">
|
|
<span class="customization-label">Per-page custom CSS and JS</span>
|
|
<form method="POST" action="/profile/{{ profile_user['username'] }}/customization/pagetype" data-customization-form>
|
|
<input type="hidden" name="value" value="{{ 1 if cust_disable_pagetype else 0 }}">
|
|
<button type="submit" class="btn btn-sm {% if cust_disable_pagetype %}btn-primary{% endif %}" data-customization-toggle="pagetype" data-confirm="{% if cust_disable_pagetype %}Re-enable your per-page custom CSS and JS?{% else %}Disable your per-page custom CSS and JS? Your saved code is kept and can be re-enabled.{% endif %}">{% if cust_disable_pagetype %}Enable{% else %}Disable{% endif %}</button>
|
|
</form>
|
|
</div>
|
|
<p class="customization-hint">Disabling hides your custom code without deleting it.</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if is_owner %}
|
|
<div class="customization-card" data-ai-correction data-username="{{ profile_user['username'] }}">
|
|
<div class="customization-head">
|
|
<span class="customization-title">AI content correction</span>
|
|
</div>
|
|
<div class="customization-row">
|
|
<span class="customization-label">Enable correction</span>
|
|
<label class="ai-correction-switch">
|
|
<input type="checkbox" data-ai-correction-toggle aria-label="Enable AI content correction" {% if ai_correction_enabled %}checked{% endif %}>
|
|
</label>
|
|
</div>
|
|
<div class="customization-row">
|
|
<span class="customization-label">Apply mode</span>
|
|
<select data-ai-correction-sync class="input-sm" aria-label="AI content correction apply mode">
|
|
<option value="async" {% if not ai_correction_sync %}selected{% endif %}>In background (faster, applies after saving)</option>
|
|
<option value="sync" {% if ai_correction_sync %}selected{% endif %}>Synchronously (wait while saving)</option>
|
|
</select>
|
|
</div>
|
|
<textarea data-ai-correction-prompt class="input-sm ai-correction-prompt" maxlength="20000" rows="3" aria-label="AI content correction prompt">{{ ai_correction_prompt }}</textarea>
|
|
<div class="customization-row">
|
|
<button type="button" class="btn btn-sm btn-primary" data-ai-correction-save>Save</button>
|
|
<span class="ai-correction-status" data-ai-correction-status role="status" aria-live="polite"></span>
|
|
</div>
|
|
<p class="customization-hint">Corrections apply to new and edited content using your API key, on posts, comments, projects, gists, direct messages and your bio. Code and source files are never changed. In background mode the correction is applied just after saving; in synchronous mode the save waits for the correction to finish.</p>
|
|
</div>
|
|
|
|
<div class="customization-card" data-ai-modifier data-username="{{ profile_user['username'] }}">
|
|
<div class="customization-head">
|
|
<span class="customization-title">AI modifier</span>
|
|
</div>
|
|
<div class="customization-row">
|
|
<span class="customization-label">Enable modifier</span>
|
|
<label class="ai-correction-switch">
|
|
<input type="checkbox" data-ai-modifier-toggle aria-label="Enable AI modifier" {% if ai_modifier_enabled %}checked{% endif %}>
|
|
</label>
|
|
</div>
|
|
<div class="customization-row">
|
|
<span class="customization-label">Apply mode</span>
|
|
<select data-ai-modifier-sync class="input-sm" aria-label="AI modifier apply mode">
|
|
<option value="async" {% if not ai_modifier_sync %}selected{% endif %}>In background (applies after saving)</option>
|
|
<option value="sync" {% if ai_modifier_sync %}selected{% endif %}>Synchronously (wait while saving)</option>
|
|
</select>
|
|
</div>
|
|
<textarea data-ai-modifier-prompt class="input-sm ai-correction-prompt" maxlength="20000" rows="3" aria-label="AI modifier prompt">{{ ai_modifier_prompt }}</textarea>
|
|
<div class="customization-row">
|
|
<button type="button" class="btn btn-sm btn-primary" data-ai-modifier-save>Save</button>
|
|
<span class="ai-correction-status" data-ai-modifier-status role="status" aria-live="polite"></span>
|
|
</div>
|
|
<p class="customization-hint">Runs only when your text contains an inline `@ai instruction` directive: it executes that instruction and removes the `@ai` marker, using your API key. Applies to posts, comments, projects, gists, direct messages and your bio. Code and source files are never touched.</p>
|
|
</div>
|
|
|
|
<div class="customization-card" data-interactions data-username="{{ profile_user['username'] }}">
|
|
<div class="customization-head">
|
|
<span class="customization-title">Devii interactive widgets</span>
|
|
</div>
|
|
<div class="customization-row">
|
|
<span class="customization-label">Enable interactive prompts</span>
|
|
<label class="ai-correction-switch">
|
|
<input type="checkbox" data-interactions-toggle aria-label="Enable Devii interactive widgets" {% if interactions_enabled %}checked{% endif %}>
|
|
</label>
|
|
</div>
|
|
<div class="customization-row">
|
|
<button type="button" class="btn btn-sm btn-primary" data-interactions-save>Save</button>
|
|
<button type="button" class="btn btn-sm" data-interactions-reset>Use site default</button>
|
|
<span class="ai-correction-status" data-interactions-status role="status" aria-live="polite"></span>
|
|
</div>
|
|
<p class="customization-hint">When enabled, Devii may ask you questions with buttons and forms (ui_prompt) in the site chat, Telegram, or CLI. The administrator sets the site default (currently {{ 'on' if interactions_default else 'off' }}); you may override it here or ask Devii via interactions_set. Source: {{ interactions_source or 'default' }}.</p>
|
|
</div>
|
|
|
|
<div class="customization-card" id="telegram-pairing" data-telegram-pairing data-username="{{ profile_user['username'] }}">
|
|
<div class="customization-head">
|
|
<span class="customization-title">Telegram</span>
|
|
</div>
|
|
<p class="customization-hint">Chat with your Devii from Telegram. Open the DevPlace bot in Telegram, then send it the four digit code below to connect this account.</p>
|
|
<div class="customization-row">
|
|
<span class="customization-label">Status</span>
|
|
<span data-telegram-status role="status" aria-live="polite">{{ 'Connected' if telegram_paired else 'Not connected' }}</span>
|
|
</div>
|
|
<div class="customization-row">
|
|
<button type="button" class="btn btn-sm btn-primary" data-telegram-request>Request pairing code</button>
|
|
<button type="button" class="btn btn-sm" data-telegram-unpair {% if not telegram_paired %}hidden{% endif %}>Disconnect</button>
|
|
</div>
|
|
<div class="telegram-code" data-telegram-code hidden>
|
|
<code class="api-key-value" data-telegram-code-value></code>
|
|
<p class="customization-hint" data-telegram-code-hint></p>
|
|
</div>
|
|
<span class="ai-correction-status" data-telegram-status-msg role="status" aria-live="polite"></span>
|
|
</div>
|
|
{% endif %}
|
|
</aside>
|
|
|
|
<div class="profile-content">
|
|
<a href="/feed" class="back-link">← Back</a>
|
|
|
|
<div class="profile-heatmap-card">
|
|
<div class="heatmap-header">
|
|
<h3 class="heatmap-title">Contributions</h3>
|
|
<span class="streak-info"><span class="icon">🔥</span> {{ streak.current }} day streak · Longest {{ streak.longest }}</span>
|
|
</div>
|
|
<div class="heatmap-scroll">
|
|
<div class="heatmap-inner">
|
|
<div class="heatmap-months">
|
|
{% for month in heatmap_months %}<span>{{ month }}</span>{% endfor %}
|
|
</div>
|
|
<div class="heatmap-grid">
|
|
{% for week in heatmap %}
|
|
<div class="heatmap-week">
|
|
{% for day in week %}
|
|
<span class="heatmap-cell level-{{ day.level }}" title="{{ day.count }} on {{ day.date }}"></span>
|
|
{% endfor %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if is_admin(user) %}
|
|
<div class="ai-usage-card" data-ai-usage data-uid="{{ profile_user['uid'] }}">
|
|
<div class="ai-usage-head">
|
|
<h3 class="ai-usage-title">AI usage (last 24h)</h3>
|
|
<span class="ai-usage-updated" data-ai-usage-updated></span>
|
|
</div>
|
|
{% if ai_quota %}
|
|
<div class="ai-quota" data-ai-quota>
|
|
<div class="ai-quota-bar">
|
|
<span class="ai-quota-fill {% if not ai_quota.unlimited and ai_quota.used_pct >= 90 %}ai-quota-danger{% endif %}" style="--quota-pct: {{ 0 if ai_quota.unlimited else ai_quota.used_pct }}%"></span>
|
|
</div>
|
|
<div class="ai-quota-meta">
|
|
<span class="ai-quota-pct">{% if ai_quota.unlimited %}No daily spend cap{% else %}{{ ai_quota.used_pct }}% of daily quota used{% endif %}</span>
|
|
<span class="ai-quota-detail">${{ '%.4f'|format(ai_quota.spent_usd) }} / {% if ai_quota.unlimited %}no cap{% else %}${{ '%.2f'|format(ai_quota.limit_usd) }}{% endif %} · {{ ai_quota.turns }} Devii turns</span>
|
|
</div>
|
|
</div>
|
|
{% if 'gateway_used_pct' in ai_quota %}
|
|
<div class="ai-quota" data-ai-quota-gateway>
|
|
<div class="ai-quota-bar">
|
|
<span class="ai-quota-fill {% if not ai_quota.gateway_unlimited and ai_quota.gateway_used_pct >= 90 %}ai-quota-danger{% endif %}" style="--quota-pct: {{ 0 if ai_quota.gateway_unlimited else ai_quota.gateway_used_pct }}%"></span>
|
|
</div>
|
|
<div class="ai-quota-meta">
|
|
<span class="ai-quota-pct">{% if ai_quota.gateway_unlimited %}No gateway daily cap{% else %}{{ ai_quota.gateway_used_pct }}% of gateway quota used{% endif %}</span>
|
|
<span class="ai-quota-detail">${{ '%.4f'|format(ai_quota.gateway_spent_usd) }} / {% if ai_quota.gateway_unlimited %}no cap{% else %}${{ '%.2f'|format(ai_quota.gateway_limit_usd) }}{% endif %} · direct /openai/v1/* cap{% if ai_quota.gateway_pooled %}, shared pool{% endif %}</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
<div class="ai-usage-body" data-ai-usage-body>
|
|
<p class="ai-usage-empty">Loading...</p>
|
|
</div>
|
|
</div>
|
|
{% elif is_self(user, profile_user['uid']) %}
|
|
<div class="ai-usage-card ai-quota-only">
|
|
<div class="ai-usage-head">
|
|
<h3 class="ai-usage-title">AI quota</h3>
|
|
</div>
|
|
{% if ai_quota %}
|
|
<div class="ai-quota" data-ai-quota>
|
|
<div class="ai-quota-bar">
|
|
<span class="ai-quota-fill {% if not ai_quota.unlimited and ai_quota.used_pct >= 90 %}ai-quota-danger{% endif %}" style="--quota-pct: {{ 0 if ai_quota.unlimited else ai_quota.used_pct }}%"></span>
|
|
</div>
|
|
<div class="ai-quota-meta">
|
|
<span class="ai-quota-pct">{% if ai_quota.unlimited %}No daily AI spend cap{% else %}{{ ai_quota.used_pct }}% of your daily AI quota used{% endif %}</span>
|
|
</div>
|
|
</div>
|
|
{% if 'gateway_used_pct' in ai_quota %}
|
|
<div class="ai-quota" data-ai-quota-gateway>
|
|
<div class="ai-quota-bar">
|
|
<span class="ai-quota-fill {% if not ai_quota.gateway_unlimited and ai_quota.gateway_used_pct >= 90 %}ai-quota-danger{% endif %}" style="--quota-pct: {{ 0 if ai_quota.gateway_unlimited else ai_quota.gateway_used_pct }}%"></span>
|
|
</div>
|
|
<div class="ai-quota-meta">
|
|
<span class="ai-quota-pct">{% if ai_quota.gateway_unlimited %}No gateway daily cap{% else %}{{ ai_quota.gateway_used_pct }}% of your gateway quota used{% endif %}</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<p class="ai-usage-empty">Quota unavailable.</p>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if correction_usage and correction_usage.calls %}
|
|
<div class="ai-usage-card correction-usage-card">
|
|
<div class="ai-usage-head">
|
|
<h3 class="ai-usage-title">AI correction usage</h3>
|
|
{% if correction_usage.last_used %}<span class="ai-usage-updated">{{ format_date(correction_usage.last_used) }}</span>{% endif %}
|
|
</div>
|
|
<div class="correction-usage-stats">
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "{:,}".format(correction_usage.calls) }}</span>
|
|
<span class="correction-usage-label">Corrections</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "{:,}".format(correction_usage.total_tokens) }}</span>
|
|
<span class="correction-usage-label">Total tokens</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "{:,}".format(correction_usage.prompt_tokens) }}</span>
|
|
<span class="correction-usage-label">Prompt tokens</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "{:,}".format(correction_usage.completion_tokens) }}</span>
|
|
<span class="correction-usage-label">Completion tokens</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "%.0f"|format(correction_usage.avg_tokens) }}</span>
|
|
<span class="correction-usage-label">Avg tokens/call</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "%.0f"|format(correction_usage.avg_latency_ms) }} ms</span>
|
|
<span class="correction-usage-label">Avg latency</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "%.1f"|format(correction_usage.avg_tokens_per_second) }}/s</span>
|
|
<span class="correction-usage-label">Avg speed</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "%.1f"|format(correction_usage.total_time_s) }} s</span>
|
|
<span class="correction-usage-label">Total time</span>
|
|
</div>
|
|
{% if correction_usage.cost_usd is defined %}
|
|
<div class="correction-usage-stat correction-usage-cost">
|
|
<span class="correction-usage-value">${{ "%.4f"|format(correction_usage.cost_usd) }}</span>
|
|
<span class="correction-usage-label">Total cost</span>
|
|
</div>
|
|
<div class="correction-usage-stat correction-usage-cost">
|
|
<span class="correction-usage-value">${{ "%.5f"|format(correction_usage.avg_cost_usd) }}</span>
|
|
<span class="correction-usage-label">Avg cost/call</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if modifier_usage and modifier_usage.calls %}
|
|
<div class="ai-usage-card correction-usage-card">
|
|
<div class="ai-usage-head">
|
|
<h3 class="ai-usage-title">AI modifier usage</h3>
|
|
{% if modifier_usage.last_used %}<span class="ai-usage-updated">{{ format_date(modifier_usage.last_used) }}</span>{% endif %}
|
|
</div>
|
|
<div class="correction-usage-stats">
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "{:,}".format(modifier_usage.calls) }}</span>
|
|
<span class="correction-usage-label">Modifications</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "{:,}".format(modifier_usage.total_tokens) }}</span>
|
|
<span class="correction-usage-label">Total tokens</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "{:,}".format(modifier_usage.prompt_tokens) }}</span>
|
|
<span class="correction-usage-label">Prompt tokens</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "{:,}".format(modifier_usage.completion_tokens) }}</span>
|
|
<span class="correction-usage-label">Completion tokens</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "%.0f"|format(modifier_usage.avg_tokens) }}</span>
|
|
<span class="correction-usage-label">Avg tokens/call</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "%.0f"|format(modifier_usage.avg_latency_ms) }} ms</span>
|
|
<span class="correction-usage-label">Avg latency</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "%.1f"|format(modifier_usage.avg_tokens_per_second) }}/s</span>
|
|
<span class="correction-usage-label">Avg speed</span>
|
|
</div>
|
|
<div class="correction-usage-stat">
|
|
<span class="correction-usage-value">{{ "%.1f"|format(modifier_usage.total_time_s) }} s</span>
|
|
<span class="correction-usage-label">Total time</span>
|
|
</div>
|
|
{% if modifier_usage.cost_usd is defined %}
|
|
<div class="correction-usage-stat correction-usage-cost">
|
|
<span class="correction-usage-value">${{ "%.4f"|format(modifier_usage.cost_usd) }}</span>
|
|
<span class="correction-usage-label">Total cost</span>
|
|
</div>
|
|
<div class="correction-usage-stat correction-usage-cost">
|
|
<span class="correction-usage-value">${{ "%.5f"|format(modifier_usage.avg_cost_usd) }}</span>
|
|
<span class="correction-usage-label">Avg cost/call</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<details class="profile-achievements">
|
|
<summary class="profile-achievements-summary">
|
|
<span class="profile-achievements-title"><span class="icon">🏆</span> Achievements</span>
|
|
<span class="profile-achievements-count">{{ badge_earned }} / {{ badge_total }}</span>
|
|
</summary>
|
|
{% for group in achievements %}
|
|
<div class="achievement-group">
|
|
<h3 class="achievement-group-title">{{ group['group'] }} <span class="achievement-group-count">{{ group['earned'] }}/{{ group['total'] }}</span></h3>
|
|
<div class="achievement-grid">
|
|
{% for item in group['items'] %}
|
|
<div class="achievement {% if item['earned'] %}earned{% else %}locked{% endif %}" title="{{ item['description'] }}">
|
|
<span class="achievement-icon">{{ item['icon'] }}</span>
|
|
<span class="achievement-body">
|
|
<span class="achievement-name">{{ item['name'] }}</span>
|
|
<span class="achievement-desc">{{ item['description'] }}</span>
|
|
</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</details>
|
|
|
|
{% if prominent_award %}
|
|
<section class="award-prominent" aria-label="Latest award">
|
|
<img src="{{ prominent_award['image_url'] }}" alt="{{ prominent_award['description'] }}" title="{{ prominent_award['description'] }}" class="award-prominent-image" loading="lazy">
|
|
<div class="award-prominent-desc rendered-content">{{ render_content(prominent_award['description']) }}</div>
|
|
<div class="award-prominent-meta">
|
|
{% if prominent_award.get('giver') %}
|
|
<a href="/profile/{{ prominent_award['giver']['username'] }}" class="award-prominent-giver">
|
|
<img src="{{ avatar_url('multiavatar', avatar_seed(prominent_award['giver']), 24) }}" class="avatar-img avatar-xs" alt="{{ prominent_award['giver']['username'] }}" loading="lazy">
|
|
<span>Given by @{{ prominent_award['giver']['username'] }}</span>
|
|
</a>
|
|
{% endif %}
|
|
{% if prominent_award.get('generated_at') %}
|
|
<span>{{ award_date(prominent_award['generated_at']) }}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if viewer_is_admin %}
|
|
<form method="POST" action="/admin/awards/{{ prominent_award['uid'] }}/revoke" class="award-revoke-form">
|
|
<button type="submit"
|
|
class="award-revoke-btn btn btn-secondary btn-sm"
|
|
data-confirm="Revoke this award from @{{ profile_user['username'] }}? It will disappear from their profile, media gallery, and avatar badge."
|
|
data-confirm-danger
|
|
data-confirm-title="Revoke award"
|
|
aria-label="Revoke award">Revoke award</button>
|
|
</form>
|
|
{% endif %}
|
|
</section>
|
|
{% endif %}
|
|
|
|
<div class="profile-tabs" id="profile-tabs" data-overflow-tabs>
|
|
<div class="overflow-tabs-strip">
|
|
{% if awards_count > 0 %}
|
|
<a href="/profile/{{ profile_user['username'] }}?tab=awards#profile-tabs" class="profile-tab {% if current_tab == 'awards' %}active{% endif %}" data-menu-icon="🏆" data-menu-label="Awards ({{ awards_count }})"><span class="icon">🏆</span> Awards ({{ awards_count }})</a>
|
|
{% endif %}
|
|
<a href="/profile/{{ profile_user['username'] }}?tab=posts#profile-tabs" class="profile-tab {% if current_tab == 'posts' %}active{% endif %}" data-menu-icon="📝" data-menu-label="Posts"><span class="icon">📝</span> Posts</a>
|
|
<a href="/profile/{{ profile_user['username'] }}?tab=projects#profile-tabs" class="profile-tab {% if current_tab == 'projects' %}active{% endif %}" data-menu-icon="🚀" data-menu-label="Projects"><span class="icon">🚀</span> Projects</a>
|
|
<a href="/profile/{{ profile_user['username'] }}?tab=gists#profile-tabs" class="profile-tab {% if current_tab == 'gists' %}active{% endif %}" data-menu-icon="📝" data-menu-label="Gists"><span class="icon">📝</span> Gists</a>
|
|
<a href="/profile/{{ profile_user['username'] }}?tab=media#profile-tabs" class="profile-tab {% if current_tab == 'media' %}active{% endif %}" data-menu-icon="🖼️" data-menu-label="Media"><span class="icon">🖼️</span> Media</a>
|
|
<a href="/profile/{{ profile_user['username'] }}?tab=activity#profile-tabs" class="profile-tab {% if current_tab == 'activity' %}active{% endif %}" data-menu-icon="📊" data-menu-label="Activity"><span class="icon">📊</span> Activity</a>
|
|
<a href="/profile/{{ profile_user['username'] }}?tab=followers#profile-tabs" class="profile-tab {% if current_tab == 'followers' %}active{% endif %}" data-menu-icon="👥" data-menu-label="Followers ({{ followers_count }})"><span class="icon">👥</span> Followers ({{ followers_count }})</a>
|
|
<a href="/profile/{{ profile_user['username'] }}?tab=following#profile-tabs" class="profile-tab {% if current_tab == 'following' %}active{% endif %}" data-menu-icon="👤" data-menu-label="Following ({{ following_count }})"><span class="icon">👤</span> Following ({{ following_count }})</a>
|
|
{% if is_owner or viewer_is_admin %}
|
|
<a href="/profile/{{ profile_user['username'] }}?tab=notifications#profile-tabs" class="profile-tab {% if current_tab == 'notifications' %}active{% endif %}" data-menu-icon="🔔" data-menu-label="Notifications"><span class="icon">🔔</span> Notifications</a>
|
|
{% endif %}
|
|
</div>
|
|
<button type="button" class="profile-tab overflow-tabs-more" aria-haspopup="menu" aria-label="More tabs" hidden><span class="icon">⋯</span> More</button>
|
|
</div>
|
|
|
|
<div class="profile-posts">
|
|
{% if current_tab == 'posts' %}
|
|
{% for item in posts %}
|
|
{% set _author = profile_user %}{% set _time = item.time_ago %}{% set _show_share = false %}{% set _show_comment_form = false %}{% include "_post_card.html" %}
|
|
{% else %}
|
|
<div class="empty-state">No posts yet.</div>
|
|
{% endfor %}
|
|
{% elif current_tab == 'projects' %}
|
|
{% for p in projects %}
|
|
<a href="/projects/{{ p['slug'] or p['uid'] }}" class="project-card">
|
|
<div class="project-card-header">
|
|
<h3 class="project-card-title">{{ render_title(p['title'], author_is_admin=is_admin(profile_user)) }}</h3>
|
|
</div>
|
|
<div class="project-card-meta">
|
|
<span class="project-status {% if p.get('status') == 'Released' %}released{% else %}dev{% endif %}">
|
|
● {{ p.get('status', 'In Development') }}
|
|
</span>
|
|
<span>{{ p.get('project_type', 'software')|replace('_', ' ')|capitalize }}</span>
|
|
</div>
|
|
<div class="project-card-desc rendered-content">{{ render_content(p.get('description', '')[:200], author_is_admin=is_admin(profile_user)) }}</div>
|
|
{% if p.get('platforms') %}
|
|
<div class="project-card-platforms">
|
|
{% for plat in p['platforms'].split(',') %}
|
|
<span class="platform-tag">{{ plat.strip() }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</a>
|
|
{% else %}
|
|
<div class="empty-state">No projects yet.</div>
|
|
{% endfor %}
|
|
{% elif current_tab == 'gists' %}
|
|
{% for entry in gists %}
|
|
{% set g = entry.gist %}
|
|
<a href="/gists/{{ g['slug'] or g['uid'] }}" class="gist-card">
|
|
<div class="gist-card-header">
|
|
<h3 class="gist-card-title">{{ render_title(g['title'], author_is_admin=is_admin(profile_user)) }}</h3>
|
|
<span class="gist-card-star">☆ {{ g.get('stars', 0) }}</span>
|
|
</div>
|
|
<div class="gist-card-meta">
|
|
<span class="gist-language-badge">📝 {{ language_name(g['language']) }}</span>
|
|
</div>
|
|
{% if g.get('description') %}
|
|
<div class="gist-card-desc rendered-content">{{ render_content(g['description'][:200], author_is_admin=is_admin(profile_user)) }}</div>
|
|
{% endif %}
|
|
<div class="gist-card-footer">
|
|
<span>{{ dt_ago(entry.gist.created_at) if entry.gist.created_at else entry.time_ago }}</span>
|
|
</div>
|
|
</a>
|
|
{% else %}
|
|
<div class="empty-state">No gists yet.</div>
|
|
{% endfor %}
|
|
{% elif current_tab == 'media' %}
|
|
{% include "_media_gallery.html" %}
|
|
{% set pagination = media_pagination %}
|
|
{% set pagination_query = "tab=media&" %}
|
|
{% include "_pagination.html" %}
|
|
{% elif current_tab == 'awards' %}
|
|
{% include "_awards_gallery.html" %}
|
|
{% set pagination = awards_pagination %}
|
|
{% set pagination_query = "tab=awards&" %}
|
|
{% include "_pagination.html" %}
|
|
{% elif current_tab == 'followers' or current_tab == 'following' %}
|
|
{% if people %}
|
|
<div class="profile-panel">
|
|
{% endif %}
|
|
{% for person in people %}
|
|
<div class="follow-row">
|
|
<a href="/profile/{{ person['username'] }}" class="follow-user">
|
|
<span class="avatar-badge"><img src="{{ avatar_url('multiavatar', avatar_seed(person), 40) }}" class="avatar-img avatar-md" alt="{{ person['username'] }}" loading="lazy">{% set _user = person %}{% include "_presence_dot.html" %}{% include "_award_badge.html" %}</span>
|
|
<div class="follow-user-info">
|
|
<span class="follow-user-name">{{ person['username'] }}</span>
|
|
{% if person['bio'] %}<span class="follow-user-bio rendered-title">{{ render_title(person['bio'], author_is_admin=is_admin(person)) }}</span>{% endif %}
|
|
</div>
|
|
</a>
|
|
{% if user and not person['is_self'] %}
|
|
{% if person['is_following'] %}
|
|
<form method="post" action="/follow/unfollow/{{ person['username'] }}">
|
|
<button type="submit" class="btn btn-ghost btn-sm"{{ guest_disabled(user) }}>Following</button>
|
|
</form>
|
|
{% else %}
|
|
<form method="post" action="/follow/{{ person['username'] }}">
|
|
<button type="submit" class="btn btn-primary btn-sm"{{ guest_disabled(user) }}>Follow</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">{% if current_tab == 'followers' %}No followers yet.{% else %}Not following anyone yet.{% endif %}</div>
|
|
{% endfor %}
|
|
{% if people %}
|
|
</div>
|
|
{% endif %}
|
|
{% if follow_pagination and follow_pagination.total_pages > 1 %}
|
|
<div class="follow-pagination">
|
|
{% if follow_pagination.has_prev %}<a class="btn btn-ghost btn-sm" href="/profile/{{ profile_user['username'] }}?tab={{ current_tab }}&page={{ follow_pagination.prev_page }}">Previous</a>{% endif %}
|
|
<span class="follow-page-label">Page {{ follow_pagination.page }} of {{ follow_pagination.total_pages }}</span>
|
|
{% if follow_pagination.has_next %}<a class="btn btn-ghost btn-sm" href="/profile/{{ profile_user['username'] }}?tab={{ current_tab }}&page={{ follow_pagination.next_page }}">Next</a>{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% elif current_tab == 'notifications' %}
|
|
{% if is_owner or viewer_is_admin %}
|
|
<div class="notif-prefs profile-panel" data-notif-prefs data-username="{{ profile_user['username'] }}">
|
|
<div class="notif-prefs-intro">
|
|
{% if not is_owner %}<p class="notif-prefs-admin">Editing notification settings for <strong>{{ profile_user['username'] }}</strong>.</p>{% endif %}
|
|
<p>Choose how you want to be notified for each event. <strong>In-app</strong> shows the notification on DevPlace; <strong>Push</strong> sends it to your subscribed devices; <strong>Telegram</strong> delivers it to your paired Telegram chat.</p>
|
|
{% if not notif_telegram_paired %}<p class="notif-prefs-hint">{% if is_owner %}Connect Telegram from the <a href="#telegram-pairing">Telegram panel</a> to enable Telegram notifications.{% else %}This user has not connected Telegram, so Telegram notifications cannot be delivered yet.{% endif %}</p>{% endif %}
|
|
</div>
|
|
<div class="notif-prefs-scroll">
|
|
<table class="notif-prefs-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Notification</th>
|
|
<th class="notif-prefs-col">In-app</th>
|
|
<th class="notif-prefs-col">Push</th>
|
|
<th class="notif-prefs-col">Telegram</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for pref in notification_prefs %}
|
|
<tr>
|
|
<td>
|
|
<span class="notif-prefs-label">{{ pref.label }}</span>
|
|
<span class="notif-prefs-desc">{{ pref.description }}</span>
|
|
</td>
|
|
<td class="notif-prefs-col">
|
|
<label class="notif-switch">
|
|
<input type="checkbox" data-notif-toggle data-type="{{ pref.key }}" data-channel="in_app" aria-label="In-app notifications for {{ pref.label }}" {% if pref.in_app %}checked{% endif %}>
|
|
<span class="notif-switch-track"></span>
|
|
</label>
|
|
</td>
|
|
<td class="notif-prefs-col">
|
|
<label class="notif-switch">
|
|
<input type="checkbox" data-notif-toggle data-type="{{ pref.key }}" data-channel="push" aria-label="Push notifications for {{ pref.label }}" {% if pref.push %}checked{% endif %}>
|
|
<span class="notif-switch-track"></span>
|
|
</label>
|
|
</td>
|
|
<td class="notif-prefs-col">
|
|
<label class="notif-switch{% if not notif_telegram_paired %} notif-switch-disabled{% endif %}">
|
|
<input type="checkbox" data-notif-toggle data-type="{{ pref.key }}" data-channel="telegram" aria-label="Telegram notifications for {{ pref.label }}" {% if pref.telegram %}checked{% endif %} {% if not notif_telegram_paired %}disabled{% endif %}>
|
|
<span class="notif-switch-track"></span>
|
|
</label>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="notif-prefs-actions">
|
|
<button type="button" class="btn btn-ghost btn-sm" data-notif-reset>Reset to defaults</button>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">Not available.</div>
|
|
{% endif %}
|
|
{% else %}
|
|
{% for act in activities %}
|
|
<div class="post-card activity-card card-link-host">
|
|
{% if act['url'] %}
|
|
{% set _href = act['url'] %}
|
|
{% set _label = (act['type'] == 'post') and 'View post' or 'View comment on post' %}
|
|
{% include "_card_link.html" %}
|
|
{% endif %}
|
|
<div class="activity-row">
|
|
<span class="activity-icon">
|
|
{% if act['type'] == 'post' %}📝{% else %}💬{% endif %}
|
|
</span>
|
|
<span class="activity-type">
|
|
{% if act['type'] == 'post' %}Posted{% else %}Commented{% endif %}
|
|
</span>
|
|
<span class="activity-time">{{ dt_ago(act['created_at']) if act['created_at'] else act['time_ago'] }}</span>
|
|
</div>
|
|
<div class="activity-content rendered-content">{{ render_content(act['content'], author_is_admin=is_admin(profile_user)) }}</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">No activity yet.</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if can_give_award %}
|
|
{% from "_macros.html" import modal %}
|
|
{% call modal('award-give-modal', 'Give award') %}
|
|
<form id="award-give-form">
|
|
<div class="auth-field auth-field-gap">
|
|
<label for="award-description">Message for @{{ profile_user['username'] }}</label>
|
|
<textarea id="award-description" name="description" maxlength="125" rows="3" required placeholder="What did they do well?"></textarea>
|
|
<small class="award-char-count" data-award-char-count>0/125</small>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary full-width">Submit award</button>
|
|
</form>
|
|
{% endcall %}
|
|
{% endif %}
|
|
{% endblock %}
|
|
{% block extra_js %}
|
|
<script type="module">
|
|
import { AwardGiver } from "{{ static_url('/static/js/AwardGiver.js') }}";
|
|
new AwardGiver();
|
|
</script>
|
|
{% endblock %}
|