diff --git a/backend/feedback/api.py b/backend/feedback/api.py index ffddc5f1..33ceb60a 100644 --- a/backend/feedback/api.py +++ b/backend/feedback/api.py @@ -30,7 +30,7 @@ async def submit_feedback(request: FeedbackRequest, user_id: str = Depends(get_c 'feedback': request.feedback } - feedback_result = await client.table('feedback').insert(feedback_data).execute() + feedback_result = await client.table('feedback').upsert(feedback_data, on_conflict='message_id').execute() if not feedback_result.data: raise HTTPException(status_code=500, detail="Failed to submit feedback") diff --git a/backend/supabase/migrations/20250703190154_feedback.sql b/backend/supabase/migrations/20250703190154_feedback.sql new file mode 100644 index 00000000..8788ad11 --- /dev/null +++ b/backend/supabase/migrations/20250703190154_feedback.sql @@ -0,0 +1,66 @@ +create table "public"."feedback" ( + "id" uuid not null default gen_random_uuid(), + "created_at" timestamp with time zone not null default now(), + "is_good" boolean not null, + "message_id" uuid, + "feedback" text +); + + +alter table "public"."feedback" enable row level security; + +CREATE UNIQUE INDEX feedback_message_id_key ON public.feedback USING btree (message_id); + +CREATE UNIQUE INDEX feedback_pkey ON public.feedback USING btree (id); + +alter table "public"."feedback" add constraint "feedback_pkey" PRIMARY KEY using index "feedback_pkey"; + +alter table "public"."feedback" add constraint "feedback_message_id_fkey" FOREIGN KEY (message_id) REFERENCES messages(message_id) not valid; + +alter table "public"."feedback" validate constraint "feedback_message_id_fkey"; + +alter table "public"."feedback" add constraint "feedback_message_id_key" UNIQUE using index "feedback_message_id_key"; + +grant delete on table "public"."feedback" to "anon"; + +grant insert on table "public"."feedback" to "anon"; + +grant references on table "public"."feedback" to "anon"; + +grant select on table "public"."feedback" to "anon"; + +grant trigger on table "public"."feedback" to "anon"; + +grant truncate on table "public"."feedback" to "anon"; + +grant update on table "public"."feedback" to "anon"; + +grant delete on table "public"."feedback" to "authenticated"; + +grant insert on table "public"."feedback" to "authenticated"; + +grant references on table "public"."feedback" to "authenticated"; + +grant select on table "public"."feedback" to "authenticated"; + +grant trigger on table "public"."feedback" to "authenticated"; + +grant truncate on table "public"."feedback" to "authenticated"; + +grant update on table "public"."feedback" to "authenticated"; + +grant delete on table "public"."feedback" to "service_role"; + +grant insert on table "public"."feedback" to "service_role"; + +grant references on table "public"."feedback" to "service_role"; + +grant select on table "public"."feedback" to "service_role"; + +grant trigger on table "public"."feedback" to "service_role"; + +grant truncate on table "public"."feedback" to "service_role"; + +grant update on table "public"."feedback" to "service_role"; + + diff --git a/frontend/src/app/(dashboard)/projects/[projectId]/thread/_hooks/useThreadData.ts b/frontend/src/app/(dashboard)/projects/[projectId]/thread/_hooks/useThreadData.ts index 7b387633..13e9ef0e 100644 --- a/frontend/src/app/(dashboard)/projects/[projectId]/thread/_hooks/useThreadData.ts +++ b/frontend/src/app/(dashboard)/projects/[projectId]/thread/_hooks/useThreadData.ts @@ -79,6 +79,7 @@ export function useThreadData(threadId: string, projectId: string): UseThreadDat thread_id: msg.thread_id || threadId, type: (msg.type || 'system') as UnifiedMessage['type'], is_llm_message: Boolean(msg.is_llm_message), + user_feedback: msg.user_feedback ?? null, content: msg.content || '', metadata: msg.metadata || '{}', created_at: msg.created_at || new Date().toISOString(), @@ -152,6 +153,7 @@ export function useThreadData(threadId: string, projectId: string): UseThreadDat thread_id: msg.thread_id || threadId, type: (msg.type || 'system') as UnifiedMessage['type'], is_llm_message: Boolean(msg.is_llm_message), + user_feedback: msg.user_feedback ?? null, content: msg.content || '', metadata: msg.metadata || '{}', created_at: msg.created_at || new Date().toISOString(), diff --git a/frontend/src/app/(dashboard)/projects/[projectId]/thread/_types/index.ts b/frontend/src/app/(dashboard)/projects/[projectId]/thread/_types/index.ts index 38f2c1cf..aa3737cb 100644 --- a/frontend/src/app/(dashboard)/projects/[projectId]/thread/_types/index.ts +++ b/frontend/src/app/(dashboard)/projects/[projectId]/thread/_types/index.ts @@ -20,6 +20,7 @@ export interface ApiMessageType extends BaseApiMessageType { metadata?: string; created_at?: string; updated_at?: string; + user_feedback?: boolean | null; } export interface StreamingToolCall { diff --git a/frontend/src/app/share/[threadId]/page.tsx b/frontend/src/app/share/[threadId]/page.tsx index a158bd18..1e72c81e 100644 --- a/frontend/src/app/share/[threadId]/page.tsx +++ b/frontend/src/app/share/[threadId]/page.tsx @@ -48,6 +48,7 @@ interface ApiMessageType extends BaseApiMessageType { avatar?: string; avatar_color?: string; }; + user_feedback?: boolean | null; } interface StreamingToolCall { @@ -401,6 +402,7 @@ export default function ThreadPage({ updated_at: msg.updated_at || new Date().toISOString(), agent_id: (msg as any).agent_id, agents: (msg as any).agents, + user_feedback: msg.user_feedback ?? null, })); setMessages(unifiedMessages); diff --git a/frontend/src/components/thread/content/ThreadContent.tsx b/frontend/src/components/thread/content/ThreadContent.tsx index fb262946..67df806f 100644 --- a/frontend/src/components/thread/content/ThreadContent.tsx +++ b/frontend/src/components/thread/content/ThreadContent.tsx @@ -904,7 +904,7 @@ export const ThreadContent: React.FC = ({ return (
- +
); })()} diff --git a/frontend/src/components/thread/feedback-modal.tsx b/frontend/src/components/thread/feedback-modal.tsx index 47e0de10..2049f2ca 100644 --- a/frontend/src/components/thread/feedback-modal.tsx +++ b/frontend/src/components/thread/feedback-modal.tsx @@ -10,13 +10,14 @@ type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error'; interface FeedbackProps { messageId: string; + initialFeedback?: boolean | null; } -export default function Feedback({ messageId }: FeedbackProps) { +export default function Feedback({ messageId, initialFeedback = null }: FeedbackProps) { const [open, setOpen] = useState(false); - const [responseIsGood, setResponseIsGood] = useState(null); + const [responseIsGood, setResponseIsGood] = useState(initialFeedback); const [feedback, setFeedback] = useState(''); - const [submitStatus, setSubmitStatus] = useState('idle'); + const [submitStatus, setSubmitStatus] = useState(initialFeedback !== null ? 'success' : 'idle'); const handleClick = (isGood: boolean) => { setResponseIsGood(isGood); diff --git a/frontend/src/components/thread/types.ts b/frontend/src/components/thread/types.ts index 1e26dbe9..f3f75c70 100644 --- a/frontend/src/components/thread/types.ts +++ b/frontend/src/components/thread/types.ts @@ -25,6 +25,7 @@ export interface UnifiedMessage { avatar?: string; avatar_color?: string; }; // Agent information from join + user_feedback?: boolean | null; // Feedback provided by current user: true (liked), false (disliked), null / undefined (no feedback) } // Helper type for parsed content - structure depends on message.type diff --git a/frontend/src/hooks/useAgentStream.ts b/frontend/src/hooks/useAgentStream.ts index 5855c279..305edb0b 100644 --- a/frontend/src/hooks/useAgentStream.ts +++ b/frontend/src/hooks/useAgentStream.ts @@ -29,6 +29,7 @@ interface ApiMessageType { avatar?: string; avatar_color?: string; }; + user_feedback?: boolean | null; } // Define the structure returned by the hook @@ -68,6 +69,7 @@ const mapApiMessagesToUnified = ( metadata: msg.metadata || '{}', created_at: msg.created_at || new Date().toISOString(), updated_at: msg.updated_at || new Date().toISOString(), + user_feedback: msg.user_feedback ?? null, agent_id: (msg as any).agent_id, agents: (msg as any).agents, })); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index ea588626..a765b077 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -76,6 +76,11 @@ export type Message = { }; }; +export interface MessageWithFeedback extends Message { + message_id: string; + user_feedback?: boolean | null; +} + export type AgentRun = { id: string; thread_id: string; @@ -574,10 +579,10 @@ export const addUserMessage = async ( } }; -export const getMessages = async (threadId: string): Promise => { +export const getMessages = async (threadId: string): Promise => { const supabase = createClient(); - let allMessages: Message[] = []; + let allMessages: MessageWithFeedback[] = []; let from = 0; const batchSize = 1000; let hasMore = true; @@ -614,6 +619,37 @@ export const getMessages = async (threadId: string): Promise => { } } + + try { + const messageIds = allMessages + .filter((m: Message) => m.type === 'assistant') + .map((m: any) => m.message_id) + .filter((id) => Boolean(id)); + + if (messageIds.length > 0) { + const { data: feedbackData, error: feedbackError } = await supabase + .from('feedback') + .select('message_id, is_good') + .in('message_id', messageIds); + + if (feedbackError) { + console.error('Error fetching feedback data:', feedbackError); + } + + const feedback = Object.fromEntries( + feedbackData?.map((feedback: { message_id: string; is_good: boolean }) => [feedback.message_id, feedback.is_good]) ?? [] + ); + + // Attach feedback to messages + allMessages = allMessages.map((msg: MessageWithFeedback) => ({ + ...msg, + user_feedback: feedback[msg.message_id] ?? null, + })); + } + } catch (feedbackAttachError) { + console.error('Failed to attach feedback metadata to messages:', feedbackAttachError); + } + console.log('[API] Messages fetched count:', allMessages.length); return allMessages;