|
{% 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') }}">
|
|
{% endblock %}
|
|
{% block content %}
|
|
<div class="profile-layout">
|
|
<aside class="profile-sidebar">
|
|
<div class="profile-card">
|
|
<div class="profile-avatar-wrap">
|
|
<img src="{{ avatar_url('multiavatar', profile_user['username'], 80) }}" class="avatar-img avatar-lg" alt="{{ profile_user['username'] }}" id="profile-avatar-preview" loading="lazy">
|
|
</div>
|
|
<h1 class="profile-name">{{ profile_user['username'] }}</h1>
|
|
|
|
<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', 0) % 100 }}%</span>
|
|
</div>
|
|
<div class="bar">
|
|
<div class="bar-fill" style="width: {{ profile_user.get('xp', 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 %}
|
|
</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">
|
|
<span class="profile-info-value bio-display" id="display-bio">{{ profile_user.get('bio', '') or 'Not set' }}</span>
|
|
<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>
|
|
<span class="profile-info-value">{{ profile_user.get('bio', '') or 'Not set' }}</span>
|
|
</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="2000" 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="2000" 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-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="width: {{ 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>
|
|
{% 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="width: {{ 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>
|
|
{% 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>
|
|
|
|
<div class="profile-tabs" data-profile-tabs>
|
|
<a href="/profile/{{ profile_user['username'] }}?tab=posts" 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" 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" 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" 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" 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" 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" 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" class="profile-tab {% if current_tab == 'notifications' %}active{% endif %}" data-menu-icon="🔔" data-menu-label="Notifications"><span class="icon">🔔</span> Notifications</a>
|
|
{% endif %}
|
|
<button type="button" class="profile-tab profile-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']) }}</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]) }}</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']) }}</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">{{ g['description'][:200] }}{% if g['description']|length > 200 %}...{% endif %}</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 == 'followers' or current_tab == 'following' %}
|
|
{% for person in people %}
|
|
<div class="follow-row">
|
|
<a href="/profile/{{ person['username'] }}" class="follow-user">
|
|
<img src="{{ avatar_url('multiavatar', person['username'], 40) }}" class="avatar-img avatar-md" alt="{{ person['username'] }}" loading="lazy">
|
|
<div class="follow-user-info">
|
|
<span class="follow-user-name">{{ person['username'] }}</span>
|
|
{% if person['bio'] %}<span class="follow-user-bio">{{ person['bio'] }}</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 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" 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.</p>
|
|
</div>
|
|
<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>
|
|
</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>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
<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">
|
|
<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>
|
|
<p class="activity-content">{{ act['content'] }}</p>
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">No activity yet.</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
{% block extra_js %}
|
|
<script type="module">
|
|
import { ProfileTabs } from "{{ static_url('/static/js/ProfileTabs.js') }}";
|
|
const tabs = document.querySelector("[data-profile-tabs]");
|
|
if (tabs) {
|
|
new ProfileTabs(tabs);
|
|
}
|
|
</script>
|
|
{% endblock %}
|