Initial commit: EGBE chatbot template
Stripped from vercel/chatbot (Apache 2.0): - Dropped @vercel/* packages and AI Gateway - Removed artifacts feature (code/text/sheet/image side panel) - Switched AI provider to @ai-sdk/openai-compatible -> EGBE LiteLLM - Replaced Vercel Blob upload with data URLs - Dropped Redis resumable streams and rate limiter (in-memory now) - Added Dockerfile (Next.js standalone) + entrypoint that runs migrations - Wired DATABASE_URL, EGBE_AI_API_URL/KEY, NEXT_PUBLIC_BASE_URL for app-deploy.sh
This commit is contained in:
commit
3e21c2334c
129 changed files with 21913 additions and 0 deletions
207
components/chat/message-actions.tsx
Normal file
207
components/chat/message-actions.tsx
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import equal from "fast-deep-equal";
|
||||
import { memo } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useCopyToClipboard } from "usehooks-ts";
|
||||
import type { Vote } from "@/lib/db/schema";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import {
|
||||
MessageAction as Action,
|
||||
MessageActions as Actions,
|
||||
} from "../ai-elements/message";
|
||||
import { CopyIcon, PencilEditIcon, ThumbDownIcon, ThumbUpIcon } from "./icons";
|
||||
|
||||
export function PureMessageActions({
|
||||
chatId,
|
||||
message,
|
||||
vote,
|
||||
isLoading,
|
||||
onEdit,
|
||||
}: {
|
||||
chatId: string;
|
||||
message: ChatMessage;
|
||||
vote: Vote | undefined;
|
||||
isLoading: boolean;
|
||||
onEdit?: () => void;
|
||||
}) {
|
||||
const { mutate } = useSWRConfig();
|
||||
const [_, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const textFromParts = message.parts
|
||||
?.filter((part) => part.type === "text")
|
||||
.map((part) => part.text)
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!textFromParts) {
|
||||
toast.error("There's no text to copy!");
|
||||
return;
|
||||
}
|
||||
|
||||
await copyToClipboard(textFromParts);
|
||||
toast.success("Copied to clipboard!");
|
||||
};
|
||||
|
||||
if (message.role === "user") {
|
||||
return (
|
||||
<Actions className="-mr-0.5 justify-end opacity-0 transition-opacity duration-150 group-hover/message:opacity-100">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{onEdit && (
|
||||
<Action
|
||||
className="size-7 text-muted-foreground/50 hover:text-foreground"
|
||||
data-testid="message-edit-button"
|
||||
onClick={onEdit}
|
||||
tooltip="Edit"
|
||||
>
|
||||
<PencilEditIcon />
|
||||
</Action>
|
||||
)}
|
||||
<Action
|
||||
className="size-7 text-muted-foreground/50 hover:text-foreground"
|
||||
onClick={handleCopy}
|
||||
tooltip="Copy"
|
||||
>
|
||||
<CopyIcon />
|
||||
</Action>
|
||||
</div>
|
||||
</Actions>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Actions className="-ml-0.5 opacity-0 transition-opacity duration-150 group-hover/message:opacity-100">
|
||||
<Action
|
||||
className="text-muted-foreground/50 hover:text-foreground"
|
||||
onClick={handleCopy}
|
||||
tooltip="Copy"
|
||||
>
|
||||
<CopyIcon />
|
||||
</Action>
|
||||
|
||||
<Action
|
||||
className="text-muted-foreground/50 hover:text-foreground"
|
||||
data-testid="message-upvote"
|
||||
disabled={vote?.isUpvoted}
|
||||
onClick={() => {
|
||||
const upvote = fetch(
|
||||
`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/vote`,
|
||||
{
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
type: "up",
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
toast.promise(upvote, {
|
||||
loading: "Upvoting Response...",
|
||||
success: () => {
|
||||
mutate<Vote[]>(
|
||||
`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/vote?chatId=${chatId}`,
|
||||
(currentVotes) => {
|
||||
if (!currentVotes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const votesWithoutCurrent = currentVotes.filter(
|
||||
(currentVote) => currentVote.messageId !== message.id
|
||||
);
|
||||
|
||||
return [
|
||||
...votesWithoutCurrent,
|
||||
{
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
isUpvoted: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
{ revalidate: false }
|
||||
);
|
||||
|
||||
return "Upvoted Response!";
|
||||
},
|
||||
error: "Failed to upvote response.",
|
||||
});
|
||||
}}
|
||||
tooltip="Upvote Response"
|
||||
>
|
||||
<ThumbUpIcon />
|
||||
</Action>
|
||||
|
||||
<Action
|
||||
className="text-muted-foreground/50 hover:text-foreground"
|
||||
data-testid="message-downvote"
|
||||
disabled={vote && !vote.isUpvoted}
|
||||
onClick={() => {
|
||||
const downvote = fetch(
|
||||
`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/vote`,
|
||||
{
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
type: "down",
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
toast.promise(downvote, {
|
||||
loading: "Downvoting Response...",
|
||||
success: () => {
|
||||
mutate<Vote[]>(
|
||||
`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/vote?chatId=${chatId}`,
|
||||
(currentVotes) => {
|
||||
if (!currentVotes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const votesWithoutCurrent = currentVotes.filter(
|
||||
(currentVote) => currentVote.messageId !== message.id
|
||||
);
|
||||
|
||||
return [
|
||||
...votesWithoutCurrent,
|
||||
{
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
isUpvoted: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
{ revalidate: false }
|
||||
);
|
||||
|
||||
return "Downvoted Response!";
|
||||
},
|
||||
error: "Failed to downvote response.",
|
||||
});
|
||||
}}
|
||||
tooltip="Downvote Response"
|
||||
>
|
||||
<ThumbDownIcon />
|
||||
</Action>
|
||||
</Actions>
|
||||
);
|
||||
}
|
||||
|
||||
export const MessageActions = memo(
|
||||
PureMessageActions,
|
||||
(prevProps, nextProps) => {
|
||||
if (!equal(prevProps.vote, nextProps.vote)) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isLoading !== nextProps.isLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue