Skip to content

Commit

Permalink
feat: support delete feedback (#174)
Browse files Browse the repository at this point in the history
* feat: support delete feedback

* support external user id for app auth

* no more requires graph index enabled
  • Loading branch information
634750802 committed Jul 2, 2024
1 parent 9f81e16 commit 1710c2e
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 42 deletions.
45 changes: 30 additions & 15 deletions src/app/api/v1/chats/[id]/messages/[messageId]/feedback/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getChat, getChatMessage } from '@/core/repositories/chat';
import { createFeedback, findFeedback } from '@/core/repositories/feedback';
import { createFeedback, deleteFeedback, findFeedback } from '@/core/repositories/feedback';
import { getTraceId } from '@/core/services/feedback/utils';
import { defineHandler } from '@/lib/next/handler';
import { notFound } from 'next/navigation';
Expand All @@ -19,13 +19,6 @@ export const GET = defineHandler(({
notFound();
}

// Only support chat with knowledge graph enabled.
if (!chat.engine_options.graph_retriever?.enable) {
return Response.json({
message: 'This conversation does not support knowledge graph feedback. (Knowledge graph not enabled for this conversation)',
}, { status: 400 });
}

if (!message.trace_url) {
return Response.json({
message: 'This conversation does not support knowledge graph feedback. (Langfuse link not recorded)',
Expand Down Expand Up @@ -55,13 +48,6 @@ export const POST = defineHandler(({
notFound();
}

// Only support chat with knowledge graph enabled.
if (!chat.engine_options.graph_retriever?.enable) {
return Response.json({
message: 'This conversation does not support knowledge graph feedback. (Knowledge graph not enabled for this conversation)',
}, { status: 400 });
}

if (!message.trace_url) {
return Response.json({
message: 'This conversation does not support knowledge graph feedback. (Langfuse link not recorded)',
Expand Down Expand Up @@ -92,4 +78,33 @@ export const POST = defineHandler(({
});
});

export const DELETE = defineHandler({
params: z.object({
id: z.coerce.number(),
messageId: z.coerce.number(),
}),
auth: 'anonymous',
}, async ({ params, auth }) => {
const chat = await getChat(params.id);
const message = await getChatMessage(params.messageId);

if (!chat || !message) {
notFound();
}

if (!message.trace_url) {
return Response.json({
message: 'This conversation does not support knowledge graph feedback. (Langfuse link not recorded)',
}, { status: 400 });
}

const traceId = getTraceId(message.trace_url);
const userId = auth.user.id!;
const feedback = await findFeedback(traceId, userId);

if (feedback) {
await deleteFeedback(feedback.id);
}
});

export const dynamic = 'force-dynamic';
74 changes: 50 additions & 24 deletions src/components/chat/message-feedback.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import type { ContentSource } from '@/components/chat/use-message-feedback';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Textarea } from '@/components/ui/textarea';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import type { Feedback } from '@/core/repositories/feedback';
import { Loader2Icon, ThumbsDownIcon, ThumbsUpIcon } from 'lucide-react';
import { type ReactElement, useEffect, useState } from 'react';

export function MessageFeedback ({ initial, source, sourceLoading, onFeedback, children }: { initial?: Feedback, source: ContentSource | undefined, sourceLoading: boolean, onFeedback: (action: 'like' | 'dislike', detail: Record<string, 'like' | 'dislike'>, comment: string) => Promise<void>, children: ReactElement }) {
export function MessageFeedback ({ initial, source, sourceLoading, onFeedback, onDeleteFeedback, children }: { initial?: Feedback, source: ContentSource | undefined, sourceLoading: boolean, onFeedback: (action: 'like' | 'dislike', detail: Record<string, 'like' | 'dislike'>, comment: string) => Promise<void>, onDeleteFeedback: () => Promise<void>, children: ReactElement }) {
const [open, setOpen] = useState(false);
const [action, setAction] = useState<'like' | 'dislike'>('like');
const [action, setAction] = useState<'like' | 'dislike'>(initial?.action ?? 'like');
const [detail, setDetail] = useState<Record<string, 'like' | 'dislike'>>(() => (initial ?? {}));
const [comment, setComment] = useState(initial?.comment ?? '');
const [running, setRunning] = useState(false);
const [deleting, setDeleting] = useState(false);

useEffect(() => {
if (initial) {
Expand All @@ -22,33 +23,34 @@ export function MessageFeedback ({ initial, source, sourceLoading, onFeedback, c
}
}, [initial]);

const disabled = running || !!initial;
const disabled = running || deleting || !!initial;
const deleteDisabled = running || deleting || !initial;

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{children}
</DialogTrigger>
<DialogContent className='space-y-4'>
<DialogContent className="space-y-4">
<DialogHeader>
<DialogTitle>
Feedback
</DialogTitle>
</DialogHeader>
<section className='space-y-2'>
<section className="space-y-2">
<h6 className="text-sm font-bold">Do you like this answer</h6>
<ToggleGroup disabled={disabled} className='w-max' type="single" value={action} onValueChange={value => setAction(value as any)}>
<ToggleGroupItem value="like" className='data-[state=on]:text-green-500 data-[state=on]:bg-green-500/5'>
<ToggleGroup disabled={disabled} className="w-max" type="single" value={action} onValueChange={value => setAction(value as any)}>
<ToggleGroupItem value="like" className="data-[state=on]:text-green-500 data-[state=on]:bg-green-500/5">
<ThumbsUpIcon className="w-4 h-4 mr-2" />
Like
</ToggleGroupItem>
<ToggleGroupItem value="dislike" className='data-[state=on]:text-red-500 data-[state=on]:bg-red-500/5'>
<ToggleGroupItem value="dislike" className="data-[state=on]:text-red-500 data-[state=on]:bg-red-500/5">
<ThumbsDownIcon className="w-4 h-4 mr-2" />
Dislike
</ToggleGroupItem>
</ToggleGroup>
</section>
<section className='space-y-2'>
<section className="space-y-2">
<h6 className="text-sm font-bold">Sources from Knowledge Graph</h6>
{!source && sourceLoading && <div className="flex gap-2 items-center"><Loader2Icon className="w-4 h-4 animate-spin repeat-infinite" /> Loading...</div>}
{source && (
Expand Down Expand Up @@ -89,20 +91,44 @@ export function MessageFeedback ({ initial, source, sourceLoading, onFeedback, c
disabled={disabled}
/>
</section>
<Button
className="w-full gap-2"
disabled={disabled}
onClick={() => {
setRunning(true);
onFeedback(action, detail, comment)
.then(() => setOpen(false))
.finally(() => {
setRunning(false);
});
}}>
{running && <Loader2Icon className="w-4 h-4 animate-spin repeat-infinite" />}
Add feedback
</Button>
<div className="flex w-full justify-end items-center gap-2">
<Button
className="gap-2"
disabled={disabled}
onClick={() => {
setRunning(true);
onFeedback(action, detail, comment)
.then(() => setOpen(false))
.finally(() => {
setRunning(false);
});
}}>
{running && <Loader2Icon className="w-4 h-4 animate-spin repeat-infinite" />}
Add feedback
</Button>
<Button
className="gap-2 hover:text-destructive hover:bg-transparent"
variant="ghost"
disabled={deleteDisabled}
type='button'
onClick={() => {
setDeleting(true);
onDeleteFeedback()
.then(() => {
setOpen(false);
setAction('like');
setComment('');
setDetail({});
})
.finally(() => {
setDeleting(false);
});
}}
>
Cancel feedback
{deleting && <Loader2Icon className="w-4 h-4 animate-spin repeat-infinite" />}
</Button>
</div>
</DialogContent>
</Dialog>
);
Expand Down
4 changes: 2 additions & 2 deletions src/components/chat/message-operations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { useState } from 'react';

export function MessageOperations ({ group }: { group: ConversationMessageGroupProps }) {
const { handleRegenerate, id } = useMyChatContext();
const { feedbackData, feedback: callFeedback, source, sourceLoading, disabled } = useMessageFeedback(id, group.assistantAnnotation.messageId, !!group.assistantAnnotation.traceURL);
const { feedbackData, feedback: callFeedback, deleteFeedback: callDeleteFeedback, source, sourceLoading, disabled } = useMessageFeedback(id, group.assistantAnnotation.messageId, !!group.assistantAnnotation.traceURL);
const [copied, setCopied] = useState(false);
if (!group.finished) {
return;
Expand All @@ -31,7 +31,7 @@ export function MessageOperations ({ group }: { group: ConversationMessageGroupP

<MessageFeedback
initial={feedbackData}
source={source} sourceLoading={sourceLoading} onFeedback={async (action, feedback, comment) => callFeedback(action, feedback, comment)}>
source={source} sourceLoading={sourceLoading} onFeedback={async (action, feedback, comment) => callFeedback(action, feedback, comment)} onDeleteFeedback={() => callDeleteFeedback()}>
<Button size="icon" variant="ghost" className="ml-auto rounded-full w-7 h-7" disabled={disabled}>
{feedbackData ? <MessageSquareHeartIcon className="w-4 h-4 text-green-500" /> : <MessageSquarePlusIcon className="w-4 h-4" />}
</Button>
Expand Down
13 changes: 13 additions & 0 deletions src/components/chat/use-message-feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface UseMessageFeedbackReturns {
sourceLoading: boolean;

feedback (action: 'like' | 'dislike', details: Record<string, 'like' | 'dislike'>, comment: string): Promise<void>;

deleteFeedback (): Promise<void>;
}

export type ContentSource = {
Expand All @@ -38,6 +40,10 @@ export function useMessageFeedback (chatId: number, messageId: number, enabled:
setActing(true);
return addFeedback(chatId, messageId, { action, knowledge_graph_detail: detail, comment }).finally(() => setActing(false));
},
deleteFeedback: () => {
setActing(true);
return deleteFeedback(chatId, messageId).finally(() => setActing(false));
},
source: contentData.data,
sourceLoading: contentData.isLoading || contentData.isValidating,
};
Expand All @@ -50,3 +56,10 @@ async function addFeedback (chatId: number, messageId: number, data: any) {
});
mutate(['get', `/api/v1/chats/${chatId}/messages/${messageId}/feedback`], data => data, true);
}

async function deleteFeedback (chatId: number, messageId: number) {
await fetch(`/api/v1/chats/${chatId}/messages/${messageId}/feedback`, {
method: 'delete',
});
mutate(['get', `/api/v1/chats/${chatId}/messages/${messageId}/feedback`], null, false);
}
8 changes: 8 additions & 0 deletions src/core/repositories/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ export async function findFeedback (traceId: UUID, user: string): Promise<Feedba
.executeTakeFirst();
}

export async function deleteFeedback (id: number): Promise<boolean> {
return await getDb()
.deleteFrom('feedback')
.where('id', '=', id)
.executeTakeFirstOrThrow()
.then(res => Number(res.numDeletedRows) === 1)
}

export async function listFeedbacks (request: PageRequest<{ chat_id?: number, message_id?: number }>) {
let builder = getDb()
.selectFrom('feedback')
Expand Down
3 changes: 2 additions & 1 deletion src/lib/next/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ async function verifyCronJobAuth(request: NextRequest): Promise<Session> {

async function verifyAppAuth(request: NextRequest): Promise<Session> {
const accessToken = request.headers.get('authorization')?.replace('Bearer ', '')?.trim();
const externalUserId = request.headers.get('x-external-user-id');
if (!accessToken) {
throw APP_AUTH_REQUIRE_AUTH_TOKEN_ERROR;
}
Expand All @@ -181,7 +182,7 @@ async function verifyAppAuth(request: NextRequest): Promise<Session> {
}
return {
user: {
id: aat.app_id,
id: externalUserId ? `${aat.app_id}:${externalUserId}` : aat.app_id,
role: 'app',
},
expires: DateTime.now().plus({ month: 12 }).toISO(),
Expand Down

0 comments on commit 1710c2e

Please sign in to comment.