Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export default function PromptLibraryDetailPage({
const handleUnstar = async () => {
if (!window.confirm('Remove this prompt from your library?')) return;
try {
await unlikeMutation.mutateAsync(params.id);
await unlikeMutation.mutateAsync({ id: params.id, promptVersionId: data?.prompt_version_id });
router.push('/prompt-library');
} catch {
toast.error('Failed to remove from library');
Expand Down
487 changes: 487 additions & 0 deletions frontend/src/components/admin/analytics/developer-metrics.tsx

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions frontend/src/components/admin/view-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@ import { AgentOptimizer } from './analytics/agent-optimizer';
import { AgentSkillOpt } from './analytics/agent-skillopt';
import { AgentDomain } from './analytics/agent-domain';
import { AgentBridge } from './analytics/agent-bridge';
import { DeveloperMetrics } from './analytics/developer-metrics';

type TopToggle = 'platform' | 'agents';

type PlatformView = 'feature_engagement' | 'login_activity' | 'user_metrics';
type PlatformView = 'feature_engagement' | 'login_activity' | 'user_metrics' | 'developer_metrics';
type AgentView = 'prompt_optimizer' | 'skill_builder' | 'domain_pdogepa' | 'bridge';

const PLATFORM_ITEMS: { id: PlatformView; label: string }[] = [
{ id: 'feature_engagement', label: 'Feature Engagement' },
{ id: 'login_activity', label: 'Login Activity' },
{ id: 'user_metrics', label: 'User Metrics' },
{ id: 'feature_engagement', label: 'Feature Engagement' },
{ id: 'login_activity', label: 'Login Activity' },
{ id: 'user_metrics', label: 'User Metrics' },
{ id: 'developer_metrics', label: 'Developer Metrics' },
];

const AGENT_ITEMS: { id: AgentView; label: string }[] = [
Expand Down Expand Up @@ -65,6 +67,8 @@ export function ViewTab() {
desc: 'Track login activity and daily, weekly, monthly active user trends' },
user_metrics: { title: 'User Metrics',
desc: 'User growth, new signups, and daily/weekly active user trends' },
developer_metrics: { title: 'Developer Metrics',
desc: 'HTTP request health, Sentry error tracking, bridge pipeline health, and optimizer session outcomes' },
prompt_optimizer: { title: 'Prompt Optimizer',
desc: 'Council optimizer runs, token consumption, and model distribution' },
skill_builder: { title: 'Skill Builder',
Expand Down Expand Up @@ -134,6 +138,7 @@ export function ViewTab() {
{toggle === 'platform' && platformView === 'feature_engagement' && <PlatformEngagement />}
{toggle === 'platform' && platformView === 'login_activity' && <PlatformLogins />}
{toggle === 'platform' && platformView === 'user_metrics' && <PlatformUsers />}
{toggle === 'platform' && platformView === 'developer_metrics' && <DeveloperMetrics />}
{toggle === 'agents' && agentView === 'prompt_optimizer' && <AgentOptimizer />}
{toggle === 'agents' && agentView === 'skill_builder' && <AgentSkillOpt />}
{toggle === 'agents' && agentView === 'domain_pdogepa' && <AgentDomain />}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/bridge/transfer-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ export function TransferDetail({
}}>
<StatCell label="Credits used" value={`${job.credits_charged}`} color="var(--primary)" />
<div style={{ width: 1, background: 'var(--border)' }} />
<StatCell label="Transfer type" value={job.reused_mapping ? 'Cached' : 'Full'} color={job.reused_mapping ? 'var(--primary)' : 'var(--primary)'} />
<StatCell label="Transfer type" value={job.reused_mapping ? 'Cached' : 'Full'} color={job.reused_mapping ? 'var(--primary)' : 'var(--text-muted)'} />
<div style={{ width: 1, background: 'var(--border)' }} />
<StatCell label="Mapping" value={mapping ? `${mapping.pair_count} pair${mapping.pair_count !== 1 ? 's' : ''}` : 'New'} color="var(--success)" />
{mapping?.avg_target_score != null && (
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/domain-prompts/domain-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function DomainCard({
fontSize: 11, color: '#5a5a60',
fontFamily: 'var(--font-geist-mono, monospace)',
}}>
{domain.dataset.row_count} data sources
{domain.dataset.row_count} Q&amp;A pairs
</span>
)}
{domain.optimized_prompt && (
Expand Down
29 changes: 23 additions & 6 deletions frontend/src/components/domain-prompts/domain-workspace.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { toast } from 'sonner';
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
import { api } from '@/lib/api';
import type { DomainPrompt, DomainListResponse, DatasetRowsResponse, QAPair, TournamentState, OptimizationRun, RunListResponse } from '@/types/domain-prompts';
Expand Down Expand Up @@ -1545,17 +1546,25 @@ export function DomainWorkspace() {
);
setPollingDomainId(capturedDomainId);
setPollingJobId(res.data.data.job_id);
} catch { setReoptimizing(false); }
} catch (err: unknown) {
setReoptimizing(false);
const detail = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail;
toast.error(typeof detail === 'string' ? detail : 'Failed to start optimization — please try again.');
}
}, [selected]);

const [confirmDelete, setConfirmDelete] = useState(false);
const handleDelete = useCallback(async () => {
if (!selected) return;
if (!window.confirm('Delete this domain and all its data? This cannot be undone.')) return;
try {
await api.delete(`/api/v1/domain-prompts/${selected.id}`);
setSelectedId(null);
setConfirmDelete(false);
void qc.invalidateQueries({ queryKey: ['domain-prompts'] });
} catch { /* ignore */ }
} catch {
toast.error('Failed to delete domain — please try again.');
setConfirmDelete(false);
}
}, [selected, qc]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const [cancelling, setCancelling] = useState(false);
Expand Down Expand Up @@ -1658,9 +1667,17 @@ export function DomainWorkspace() {
{recovering ? 'Restoring…' : 'Restore to Ready'}
</button>
)}
<button className="ply-btn ply-btn-sm" aria-label="Delete domain" onClick={handleDelete} title="Delete domain">
<Icon name="trash" size={13} />
</button>
{confirmDelete ? (
<>
<span style={{ fontSize: 12, color: 'var(--danger)' }}>Delete?</span>
<button className="ply-btn ply-btn-sm" style={{ color: 'var(--danger)' }} onClick={handleDelete} aria-label="Confirm delete">Yes</button>
<button className="ply-btn ply-btn-sm" onClick={() => setConfirmDelete(false)} aria-label="Cancel delete">No</button>
</>
) : (
<button className="ply-btn ply-btn-sm" aria-label="Delete domain" onClick={() => setConfirmDelete(true)} title="Delete domain">
<Icon name="trash" size={13} />
</button>
)}
<button
className="ply-btn ply-btn-primary"
onClick={() => setShowNew(true)}
Expand Down
9 changes: 0 additions & 9 deletions frontend/src/components/layout/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,6 @@ export function Header() {
API keys
</Link>

<button style={{ height: 28, padding: '0 10px', borderRadius: 6, border: '1px solid #2a2a2e',
background: 'transparent', fontSize: 12, color: '#b5b5ba', cursor: 'pointer',
display: 'flex', alignItems: 'center', gap: 6, fontFamily: 'inherit' }}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
<path d="M8.6 10.5l6.8-4M8.6 13.5l6.8 4"/>
</svg>
Share
</button>
</div>
</header>
);
Expand Down
33 changes: 30 additions & 3 deletions frontend/src/components/layout/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,22 @@ function formatTokens(n: number): string {
return String(n);
}

function TokenCard({ tokenBalance }: { tokenBalance: number }) {
function TokenCard({ tokenBalance }: { tokenBalance: number | undefined }) {
if (tokenBalance === undefined) {
return (
<div className="ply-card" style={{ padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: 8, boxShadow: 'none' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}>Tokens</span>
<div style={{ width: 40, height: 12, borderRadius: 4, background: 'var(--surface-2)', animation: 'pulse 1.5s ease-in-out infinite' }} />
</div>
<div className="ply-progress">
<i style={{ width: '0%', background: 'var(--surface-2)' }} />
</div>
<div style={{ width: '70%', height: 10, borderRadius: 4, background: 'var(--surface-2)', animation: 'pulse 1.5s ease-in-out infinite' }} />
</div>
);
}

// Clamp display at 0 — never reveal the internal overdraft buffer to users.
const displayed = Math.max(0, tokenBalance);
const isDepleted = displayed === 0;
Expand Down Expand Up @@ -123,7 +138,7 @@ function TokenCard({ tokenBalance }: { tokenBalance: number }) {
}

function RecentSessions() {
const { data } = useQuery<SessionsGrouped>({
const { data, isLoading, isError } = useQuery<SessionsGrouped>({
queryKey: ['sessions'],
queryFn: async () => {
const res = await api.get<{ data: SessionsGrouped }>('/api/v1/chat/sessions');
Expand All @@ -132,6 +147,18 @@ function RecentSessions() {
staleTime: 60_000,
});

if (isLoading) {
return (
<div style={{ padding: '4px 10px', display: 'flex', flexDirection: 'column', gap: 6 }}>
{[80, 65, 90].map((w, i) => (
<div key={i} style={{ height: 28, borderRadius: 6, background: 'var(--surface-2)', width: `${w}%`, animation: 'pulse 1.5s ease-in-out infinite' }} />
))}
</div>
);
}

if (isError) return null;

const sessions: SessionSummary[] = data
? [...data.today, ...data.last_7_days, ...data.last_30_days, ...data.older].slice(0, 5)
: [];
Expand Down Expand Up @@ -185,7 +212,7 @@ export function Sidebar() {
staleTime: 1000 * 60 * 5,
});

const tokenBalance = fetchedUser?.token_balance ?? TOKEN_START;
const tokenBalance = fetchedUser?.token_balance;

return (
<aside style={{
Expand Down
15 changes: 7 additions & 8 deletions frontend/src/components/optimize/optimize-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ import { ChatInput } from './chat-input';
import { ResultPanel } from './result-panel';
import type { ChatTurn, JobResult, SessionDetail, TemplateListResponse } from '@/types/api';

const TEMPLATE_TO_CATEGORY_SLUG: Record<string, string> = {
coding: 'code-generation',
writing: 'writing-content',
'customer-support': 'qa-rag',
analysis: 'analysis-reasoning',
};

function TemplatePickerModal({
data,
onSelect,
Expand Down Expand Up @@ -163,14 +170,6 @@ export function OptimizeChat() {
// Incremented on every chip click so ChatInput's useEffect fires even if slug didn't change
const [categoryNonce, setCategoryNonce] = useState(0);

// Maps template seed categories → prompt category slugs
const TEMPLATE_TO_CATEGORY_SLUG: Record<string, string> = {
coding: 'code-generation',
writing: 'writing-content',
'customer-support': 'qa-rag',
analysis: 'analysis-reasoning',
};

const { data: templatesData } = useQuery({
queryKey: ['templates'],
queryFn: async () => {
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/components/optimize/result-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ export function ResultCard({ result }: { result: JobResult }) {
const [showOriginal, setShowOriginal] = useState(false);

const handleCopy = async () => {
await navigator.clipboard.writeText(result.optimized_prompt);
setCopied(true);
toast.success('Copied to clipboard');
setTimeout(() => setCopied(false), 2000);
try {
await navigator.clipboard.writeText(result.optimized_prompt);
setCopied(true);
toast.success('Copied to clipboard');
setTimeout(() => setCopied(false), 2000);
} catch {
toast.error('Failed to copy — please copy manually.');
}
};

return (
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/hooks/use-favorites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,13 @@ export function useLikeMutation() {
export function useUnlikeMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) => favoritesApi.remove(id),
onSuccess: (_data, id) => {
mutationFn: ({ id }: { id: string; promptVersionId?: string }) => favoritesApi.remove(id),
onSuccess: (_data, { id, promptVersionId }) => {
qc.invalidateQueries({ queryKey: favoriteKeys.lists() });
qc.removeQueries({ queryKey: favoriteKeys.detail(id) });
if (promptVersionId) {
qc.invalidateQueries({ queryKey: favoriteKeys.status(promptVersionId) });
}
},
});
}
Expand Down
14 changes: 12 additions & 2 deletions frontend/src/hooks/use-job-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export function useJobStream(jobId: string | null): UseJobStreamResult {
useEffect(() => {
if (!jobId) return;

// Abort any previous in-flight stream before starting a new one.
abortRef.current?.abort();
const ctrl = new AbortController();
abortRef.current = ctrl;

Expand All @@ -117,6 +119,13 @@ export function useJobStream(jobId: string | null): UseJobStreamResult {
(async () => {
const { data: { session } } = await supabase.auth.getSession();
const token = session?.access_token ?? '';

if (!token) {
setError('Session expired — please refresh the page');
setStatus('failed');
return;
}
Comment on lines 120 to +128

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/src/hooks/use-job-stream.ts

Repository: ananthanarayanan431/promptly

Length of output: 8307


Correct the race condition in the missing-token branch and replace raw fetch with the Axios instance.

The getSession() call at line 120 is not bound to the abort signal. If the effect cancels while waiting for the session, the stale closure continues and overwrites the UI state with a "failed" error at lines 124‑125. Check ctrl.signal.aborted immediately after awaiting the session and before mutating state.

Additionally, the raw fetch calls at lines 39 and 130 violate the coding guideline requiring all frontend HTTP requests to use the centralized axios instance in src/lib/api.ts. This instance handles token injection, 401 redirects, and abort signaling consistently.

Proposed fix for race condition
       const { data: { session } } = await supabase.auth.getSession();
+      if (ctrl.signal.aborted) return;
       const token = session?.access_token ?? '';

       if (!token) {
+        if (ctrl.signal.aborted) return;
         setError('Session expired — please refresh the page');
         setStatus('failed');
         return;
       }

Refactor HTTP calls: Replace fetch with axios.get/post from src/lib/api.ts to comply with frontend network standards.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data: { session } } = await supabase.auth.getSession();
const token = session?.access_token ?? '';
if (!token) {
setError('Session expired — please refresh the page');
setStatus('failed');
return;
}
const { data: { session } } = await supabase.auth.getSession();
if (ctrl.signal.aborted) return;
const token = session?.access_token ?? '';
if (!token) {
if (ctrl.signal.aborted) return;
setError('Session expired — please refresh the page');
setStatus('failed');
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/hooks/use-job-stream.ts` around lines 120 - 127, The
missing-token branch in use-job-stream is vulnerable to a stale update because
getSession() is not checked against ctrl.signal before setting failed state;
after awaiting the session, verify the abort signal and return early before
calling setError or setStatus. Also replace the raw fetch usage in
use-job-stream with the shared Axios instance from src/lib/api.ts, updating the
HTTP calls in the stream setup and request path to use axios.get/axios.post so
token handling, 401 behavior, and abort support stay centralized.


try {
const res = await fetch(`${API_URL}/api/v1/chat/jobs/${jobId}/stream`, {
headers: { Authorization: `Bearer ${token}` },
Expand All @@ -138,7 +147,7 @@ export function useJobStream(jobId: string | null): UseJobStreamResult {
let buf = '';
let terminal = false;

while (true) {
outer: while (true) {
const { done, value } = await reader.read();
if (done) break;

Expand All @@ -156,7 +165,8 @@ export function useJobStream(jobId: string | null): UseJobStreamResult {
terminal = true;
} else if (ev.step === 'failed') {
if (ev.error === 'Stream timeout') {
break;
// Server closed the stream; fall through to polling fallback.
break outer;
}
setError(ev.error ?? 'Optimization failed');
setStatus('failed');
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const registerSchema = z.object({
export type RegisterFormData = z.infer<typeof registerSchema>;

export const optimizePromptSchema = z.object({
prompt: z.string().min(10, { message: 'Prompt must be at least 10 characters long' }).optional().or(z.literal('')),
prompt: z.string().min(10, { message: 'Prompt must be at least 10 characters long' }).optional(),
prompt_id: z.string().uuid().optional(),
name: z.string().optional(),
feedback: z.string().optional(),
Expand Down
37 changes: 1 addition & 36 deletions frontend/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,45 +442,10 @@ export interface ApiKeyCreatedResponse extends ApiKeyResponse {
key: string; // shown only once
}

export interface TransferJobSummary {
id: string;
source_model: string;
target_model: string;
status: string;
reused_mapping: boolean;
credits_charged: number;
source_prompt: string;
adapted_prompt: string | null;
error_message: string | null;
created_at: string;
redis_job_id: string | null;
token_count?: number | null;
mapping_text?: string | null;
}

export interface TransferJobListResponse {
jobs: TransferJobSummary[];
}
export type { TransferJobSummary, TransferJobListResponse } from '@/types/bridge';

// ── Admin ─────────────────────────────────────────────────────────────────

export interface UserActivitySession {
id: string;
title: string | null;
message_count: number;
token_count: number;
created_at: string;
}

export interface UserActivity {
email: string;
session_count: number;
total_tokens_consumed: number;
first_seen: string;
feature_counts: Record<string, number>;
sessions: UserActivitySession[];
}

export interface AdminUserItem {
id: string;
email: string;
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/types/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export interface TransferJobSummary {
error_message: string | null;
created_at: string;
redis_job_id: string | null;
token_count?: number | null;
mapping_text?: string | null;
}

export interface TransferJobListResponse {
Expand Down
Loading
Loading