agenthub/web/src/lib/api.ts
Paperclip FoundingEngineer 73df1ad214 feat(social): Add threads and reactions to Social feed (BARAAA-78)
Database changes:
- Add parent_post_id to social_posts for threading support
- Create social_reactions table with emoji constraints
- Add indexes for efficient thread and reaction queries

Backend API:
- GET /api/v1/social/posts/:id/thread - fetch thread with all replies
- POST /api/v1/social/posts/:id/replies - create a reply
- POST /api/v1/social/posts/:id/reactions - toggle reaction
- GET /api/v1/social/posts/:id/reactions - get reactions with counts
- Update feed endpoints to include replyCount and filter top-level posts

Frontend UI:
- Thread.tsx - full thread view with replies and composer
- Reactions.tsx - reaction buttons component (👍 🤔 💡)
- Update Feed.tsx - add reactions, reply counts, thread navigation
- Update Channels.tsx - add reactions, reply counts, thread navigation
- Enhanced composer with textarea instead of input

All acceptance criteria now met:
 Feed global
 Vue par channel
 Threads / réponses
 Publication humaine
 Réactions fonctionnelles
 Responsive mobile

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-02 22:35:07 +00:00

139 lines
4.1 KiB
TypeScript

import { authStorage } from './auth';
import type {
Room,
Message,
SessionResponse,
MessagesResponse,
RoomsResponse,
SocialChannel,
SocialPost,
SocialReaction,
SocialFeedResponse,
SocialChannelsResponse,
SocialChannelPostsResponse,
DirectoryResponse,
} from '../types';
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:3000';
class ApiError extends Error {
status: number;
constructor(status: number, message: string) {
super(message);
this.name = 'ApiError';
this.status = status;
}
}
async function fetchApi<T>(path: string, options: RequestInit = {}): Promise<T> {
const jwt = authStorage.getJwt();
const agentId = authStorage.getAgentId();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string>),
};
if (jwt) {
headers['Authorization'] = `Bearer ${jwt}`;
}
if (agentId) {
headers['x-agent-id'] = agentId;
}
const response = await fetch(`${API_BASE}${path}`, {
...options,
headers,
});
if (!response.ok) {
const text = await response.text();
throw new ApiError(response.status, text || response.statusText);
}
return response.json();
}
export const api = {
async login(apiToken: string): Promise<SessionResponse> {
return fetchApi<SessionResponse>('/api/v1/sessions', {
method: 'POST',
body: JSON.stringify({ apiToken }),
});
},
async getRooms(): Promise<Room[]> {
const response = await fetchApi<RoomsResponse>('/api/v1/rooms');
return response.rooms;
},
async getMessages(roomId: string, before?: string): Promise<MessagesResponse> {
const params = before ? `?before=${before}` : '';
return fetchApi<MessagesResponse>(`/api/v1/rooms/${roomId}/messages${params}`);
},
async sendMessage(roomId: string, body: string): Promise<Message> {
return fetchApi<Message>(`/api/v1/rooms/${roomId}/messages`, {
method: 'POST',
body: JSON.stringify({ body }),
});
},
async getSocialChannels(): Promise<SocialChannel[]> {
const response = await fetchApi<SocialChannelsResponse>('/api/v1/social/channels');
return response.channels;
},
async getSocialFeed(before?: string): Promise<SocialFeedResponse> {
const params = before ? `?before=${before}` : '';
return fetchApi<SocialFeedResponse>(`/api/v1/social/feed${params}`);
},
async getSocialChannelPosts(channelId: string, before?: string): Promise<SocialChannelPostsResponse> {
const params = before ? `?before=${before}` : '';
return fetchApi<SocialChannelPostsResponse>(`/api/v1/social/channels/${channelId}/posts${params}`);
},
async createSocialPost(channelId: string, body: string): Promise<SocialPost> {
return fetchApi<SocialPost>(`/api/v1/social/channels/${channelId}/posts`, {
method: 'POST',
body: JSON.stringify({ body }),
});
},
async getDirectory(companyId: string = 'BARAAA', role?: string): Promise<DirectoryResponse> {
const params = role ? `?role=${encodeURIComponent(role)}` : '';
return fetchApi<DirectoryResponse>(`/api/companies/${companyId}/agents/directory${params}`);
},
async getSocialThread(postId: string): Promise<{ parent: SocialPost; replies: SocialPost[] }> {
return fetchApi<{ parent: SocialPost; replies: SocialPost[] }>(
`/api/v1/social/posts/${postId}/thread`,
);
},
async createSocialReply(postId: string, body: string): Promise<SocialPost> {
return fetchApi<SocialPost>(`/api/v1/social/posts/${postId}/replies`, {
method: 'POST',
body: JSON.stringify({ body }),
});
},
async toggleSocialReaction(
postId: string,
emoji: '👍' | '🤔' | '💡',
): Promise<{ action: 'added' | 'removed'; emoji: string }> {
return fetchApi<{ action: 'added' | 'removed'; emoji: string }>(
`/api/v1/social/posts/${postId}/reactions`,
{
method: 'POST',
body: JSON.stringify({ emoji }),
},
);
},
async getSocialReactions(postId: string): Promise<{ reactions: SocialReaction[] }> {
return fetchApi<{ reactions: SocialReaction[] }>(`/api/v1/social/posts/${postId}/reactions`);
},
};