review.ts
1import { writable, derived } from 'svelte/store';
2import type { ReviewThread, Review } from '$lib/types/index';
3import { fileCommentCounts } from './files.ts';
4
5export const threads = writable<ReviewThread[]>([]);
6export const reviewStatus = writable<Review['status']>('pending');
7
8export const threadsByFile = derived(threads, ($threads) => {
9 const map = new Map<string, ReviewThread[]>();
10 for (const t of $threads) {
11 const existing = map.get(t.filePath) || [];
12 existing.push(t);
13 map.set(t.filePath, existing);
14 }
15 return map;
16});
17
18export const stats = derived(threads, ($threads) => {
19 const files = new Set($threads.map((t) => t.filePath));
20 return {
21 totalThreads: $threads.length,
22 totalComments: $threads.reduce((sum, t) => sum + t.comments.length, 0),
23 filesWithComments: files.size,
24 unresolvedThreads: $threads.filter((t) => !t.resolved).length
25 };
26});
27
28export async function loadComments(filePath?: string) {
29 const params = filePath ? `?file=${encodeURIComponent(filePath)}` : '';
30 const res = await fetch(`/api/comments${params}`);
31 const data = await res.json();
32 if (data.threads) {
33 if (filePath) {
34 // Merge with existing threads for other files
35 threads.update((existing) => {
36 const otherFiles = existing.filter((t) => t.filePath !== filePath);
37 return [...otherFiles, ...data.threads];
38 });
39 } else {
40 threads.set(data.threads);
41 }
42 }
43 // Update comment counts
44 updateCommentCounts();
45}
46
47function updateCommentCounts() {
48 threads.subscribe(($threads) => {
49 const counts: Record<string, number> = {};
50 for (const t of $threads) {
51 counts[t.filePath] = (counts[t.filePath] || 0) + 1;
52 }
53 fileCommentCounts.set(counts);
54 })();
55}
56
57export async function addComment(
58 filePath: string,
59 lineNumber: number,
60 side: 'old' | 'new',
61 text: string,
62 type: 'comment' | 'suggestion' | 'question' = 'comment',
63 threadId?: string,
64 originalLineContent?: string
65) {
66 const res = await fetch('/api/comments', {
67 method: 'POST',
68 headers: { 'Content-Type': 'application/json' },
69 body: JSON.stringify({ filePath, lineNumber, side, text, type, threadId, originalLineContent })
70 });
71 const thread = await res.json();
72 if (thread.id) {
73 threads.update((existing) => {
74 const idx = existing.findIndex((t) => t.id === thread.id);
75 if (idx >= 0) {
76 existing[idx] = thread;
77 return [...existing];
78 }
79 return [...existing, thread];
80 });
81 updateCommentCounts();
82 }
83 return thread;
84}
85
86export async function resolveThread(threadId: string) {
87 await fetch('/api/comments', {
88 method: 'PATCH',
89 headers: { 'Content-Type': 'application/json' },
90 body: JSON.stringify({ threadId, action: 'resolve' })
91 });
92 threads.update((existing) => {
93 const thread = existing.find((t) => t.id === threadId);
94 if (thread) thread.resolved = !thread.resolved;
95 return [...existing];
96 });
97}
98
99export async function deleteThread(threadId: string) {
100 await fetch('/api/comments', {
101 method: 'DELETE',
102 headers: { 'Content-Type': 'application/json' },
103 body: JSON.stringify({ threadId })
104 });
105 threads.update((existing) => existing.filter((t) => t.id !== threadId));
106 updateCommentCounts();
107}
108
109export async function checkOutdated() {
110 const res = await fetch('/api/comments', {
111 method: 'PATCH',
112 headers: { 'Content-Type': 'application/json' },
113 body: JSON.stringify({ action: 'check-outdated' })
114 });
115 const data = await res.json();
116 if (data.threads) {
117 threads.set(data.threads);
118 updateCommentCounts();
119 }
120 return data.outdatedCount || 0;
121}
122
123export async function submitReview(status: Review['status'], summary?: string) {
124 const res = await fetch('/api/review', {
125 method: 'POST',
126 headers: { 'Content-Type': 'application/json' },
127 body: JSON.stringify({ status, summary })
128 });
129 const review = await res.json();
130 reviewStatus.set(review.status);
131 return review;
132}