tr]:last:border-b-0", className)}
38 | {...props}
39 | />
40 | );
41 | }
42 |
43 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
44 | return (
45 |
53 | );
54 | }
55 |
56 | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
57 | return (
58 | [role=checkbox]]:translate-y-[2px]",
62 | className
63 | )}
64 | {...props}
65 | />
66 | );
67 | }
68 |
69 | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
70 | return (
71 | | [role=checkbox]]:translate-y-[2px]",
75 | className
76 | )}
77 | {...props}
78 | />
79 | );
80 | }
81 |
82 | function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
83 | return (
84 |
89 | );
90 | }
91 |
92 | export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
93 |
--------------------------------------------------------------------------------
/apps/web/src/features/chat/kit/code-block.tsx:
--------------------------------------------------------------------------------
1 | /** biome-ignore-all lint/security/noDangerouslySetInnerHtml: */
2 | "use client";
3 |
4 | import { useTheme } from "next-themes";
5 | import type React from "react";
6 | import { useEffect, useState } from "react";
7 | import { codeToHtml } from "shiki";
8 | import { cn } from "@/lib/utils";
9 |
10 | export type CodeBlockProps = {
11 | children?: React.ReactNode;
12 | className?: string;
13 | } & React.HTMLProps;
14 |
15 | function CodeBlock({ children, className, ...props }: CodeBlockProps) {
16 | return (
17 |
25 | {children}
26 |
27 | );
28 | }
29 |
30 | export type CodeBlockCodeProps = {
31 | code: string;
32 | language?: string;
33 | theme?: string;
34 | className?: string;
35 | } & React.HTMLProps;
36 |
37 | function CodeBlockCode({
38 | code,
39 | language = "tsx",
40 | theme = "github-light",
41 | className,
42 | ...props
43 | }: CodeBlockCodeProps) {
44 | const { theme: appTheme } = useTheme();
45 | const [highlightedHtml, setHighlightedHtml] = useState(null);
46 |
47 | useEffect(() => {
48 | async function highlight() {
49 | if (!code || typeof code !== "string") {
50 | setHighlightedHtml(`${code || ""} `);
51 | return;
52 | }
53 |
54 | try {
55 | const html = await codeToHtml(code, {
56 | lang: language,
57 | theme: appTheme === "dark" ? "github-dark" : "github-light",
58 | });
59 | setHighlightedHtml(html);
60 | } catch (error) {
61 | setHighlightedHtml(`${code} `);
62 | }
63 | }
64 | highlight();
65 | }, [code, language, theme, appTheme]);
66 |
67 | const classNames = cn("w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4", className);
68 |
69 | // SSR fallback: render plain code if not hydrated yet
70 | return highlightedHtml ? (
71 |
72 | ) : (
73 |
74 |
75 | {code}
76 |
77 |
78 | );
79 | }
80 |
81 | export type CodeBlockGroupProps = React.HTMLAttributes;
82 |
83 | function CodeBlockGroup({ children, className, ...props }: CodeBlockGroupProps) {
84 | return (
85 |
86 | {children}
87 |
88 | );
89 | }
90 |
91 | export { CodeBlockGroup, CodeBlockCode, CodeBlock };
92 |
--------------------------------------------------------------------------------
/apps/api/src/pkg/middleware/clerk-auth.ts:
--------------------------------------------------------------------------------
1 | // From: https://github.com/honojs/middleware/blob/main/packages/clerk-auth/src/index.ts
2 |
3 | import type { ClerkClient, ClerkOptions } from "@clerk/backend";
4 | import { createClerkClient } from "@clerk/backend";
5 | import type { Context, MiddlewareHandler } from "hono";
6 | import { env } from "hono/adapter";
7 |
8 | type ClerkAuth = ReturnType>["toAuth"]>;
9 |
10 | declare module "hono" {
11 | interface ContextVariableMap {
12 | clerk: ClerkClient;
13 | clerkAuth: ClerkAuth;
14 | }
15 | }
16 |
17 | export const getAuth = (c: Context) => {
18 | const clerkAuth = c.get("clerkAuth");
19 |
20 | return clerkAuth;
21 | };
22 |
23 | export const getUserId = (c: Context) => {
24 | const auth = getAuth(c);
25 | if (!auth?.userId) {
26 | throw new Error("Unauthorized");
27 | }
28 | return auth.userId;
29 | };
30 |
31 | type ClerkEnv = {
32 | CLERK_SECRET_KEY: string;
33 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: string;
34 | CLERK_API_URL: string;
35 | CLERK_API_VERSION: string;
36 | };
37 |
38 | export const auth = (options?: ClerkOptions): MiddlewareHandler => {
39 | return async (c, next) => {
40 | const clerkEnv = env(c);
41 | const { secretKey, publishableKey, apiUrl, apiVersion, ...rest } = options || {
42 | secretKey: clerkEnv.CLERK_SECRET_KEY || "",
43 | publishableKey: clerkEnv.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY || "",
44 | apiUrl: clerkEnv.CLERK_API_URL,
45 | apiVersion: clerkEnv.CLERK_API_VERSION,
46 | };
47 | if (!secretKey) {
48 | throw new Error("Missing Clerk Secret key");
49 | }
50 |
51 | if (!publishableKey) {
52 | throw new Error("Missing Clerk Publishable key");
53 | }
54 |
55 | const clerkClient = createClerkClient({
56 | ...rest,
57 | apiUrl,
58 | apiVersion,
59 | secretKey,
60 | publishableKey,
61 | });
62 |
63 | const requestState = await clerkClient.authenticateRequest(c.req.raw, {
64 | ...rest,
65 | secretKey,
66 | publishableKey,
67 | });
68 |
69 | if (requestState.headers) {
70 | requestState.headers.forEach((value, key) => c.res.headers.append(key, value));
71 |
72 | const locationHeader = requestState.headers.get("location");
73 |
74 | if (locationHeader) {
75 | return c.redirect(locationHeader, 307);
76 | } else if (requestState.status === "handshake") {
77 | throw new Error("Clerk: unexpected handshake without redirect");
78 | }
79 | }
80 |
81 | c.set("clerkAuth", requestState.toAuth());
82 | c.set("clerk", clerkClient);
83 |
84 | await next();
85 | };
86 | };
87 |
88 | export const requireAuth: MiddlewareHandler = async (c, next) => {
89 | const auth = getAuth(c);
90 | if (!auth?.userId) {
91 | return c.text("Unauthorized", 401);
92 | }
93 | await next();
94 | };
95 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import type * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { ChevronRight, MoreHorizontal } from "lucide-react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
8 | return ;
9 | }
10 |
11 | function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12 | return (
13 |
21 | );
22 | }
23 |
24 | function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25 | return (
26 |
31 | );
32 | }
33 |
34 | function BreadcrumbLink({
35 | asChild,
36 | className,
37 | ...props
38 | }: React.ComponentProps<"a"> & {
39 | asChild?: boolean;
40 | }) {
41 | const Comp = asChild ? Slot : "a";
42 |
43 | return (
44 |
49 | );
50 | }
51 |
52 | function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53 | return (
54 |
62 | );
63 | }
64 |
65 | function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">) {
66 | return (
67 | svg]:size-3.5", className)}
72 | {...props}
73 | >
74 | {children ?? }
75 |
76 | );
77 | }
78 |
79 | function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span">) {
80 | return (
81 |
88 |
89 | More
90 |
91 | );
92 | }
93 |
94 | export {
95 | Breadcrumb,
96 | BreadcrumbList,
97 | BreadcrumbItem,
98 | BreadcrumbLink,
99 | BreadcrumbPage,
100 | BreadcrumbSeparator,
101 | BreadcrumbEllipsis,
102 | };
103 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import type * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 | import { Loader2 } from "lucide-react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const buttonVariants = cva(
9 | "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
10 | {
11 | variants: {
12 | variant: {
13 | default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
16 | outline:
17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
18 | secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
24 | sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5",
25 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
26 | icon: "size-9",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | );
35 |
36 | function Button({
37 | className,
38 | variant,
39 | size,
40 | asChild = false,
41 | isLoading = false,
42 | children,
43 | disabled,
44 | ...props
45 | }: React.ComponentProps<"button"> &
46 | VariantProps & {
47 | asChild?: boolean;
48 | isLoading?: boolean;
49 | }) {
50 | const Comp = asChild ? Slot : "button";
51 |
52 | if (size === "icon" && isLoading) {
53 | return (
54 |
60 |
61 |
62 | );
63 | }
64 |
65 | return (
66 |
72 | {children}
73 | {isLoading && (
74 |
78 | )}
79 |
80 | );
81 | }
82 |
83 | Button.displayName = "Button";
84 |
85 | export { Button, buttonVariants };
86 |
--------------------------------------------------------------------------------
/apps/web/src/features/chat/kit/message.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
2 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
3 | import { cn } from "@/lib/utils";
4 | import { Markdown } from "./markdown";
5 |
6 | export type MessageProps = {
7 | children: React.ReactNode;
8 | className?: string;
9 | } & React.HTMLProps;
10 |
11 | const Message = ({ children, className, ...props }: MessageProps) => (
12 |
13 | {children}
14 |
15 | );
16 |
17 | export type MessageAvatarProps = {
18 | src: string;
19 | alt: string;
20 | fallback?: string;
21 | delayMs?: number;
22 | className?: string;
23 | };
24 |
25 | const MessageAvatar = ({ src, alt, fallback, delayMs, className }: MessageAvatarProps) => {
26 | return (
27 |
28 |
29 | {fallback && {fallback}}
30 |
31 | );
32 | };
33 |
34 | export type MessageContentProps = {
35 | children: React.ReactNode;
36 | markdown?: boolean;
37 | className?: string;
38 | } & React.ComponentProps &
39 | React.HTMLProps;
40 |
41 | const MessageContent = ({
42 | children,
43 | markdown = false,
44 | className,
45 | ...props
46 | }: MessageContentProps) => {
47 | const classNames = cn(
48 | "prose whitespace-normal break-words rounded-lg bg-secondary p-2 text-foreground",
49 | className
50 | );
51 |
52 | return markdown ? (
53 |
54 | {children as string}
55 |
56 | ) : (
57 |
58 | {children}
59 |
60 | );
61 | };
62 |
63 | export type MessageActionsProps = {
64 | children: React.ReactNode;
65 | className?: string;
66 | } & React.HTMLProps;
67 |
68 | const MessageActions = ({ children, className, ...props }: MessageActionsProps) => (
69 |
70 | {children}
71 |
72 | );
73 |
74 | export type MessageActionProps = {
75 | className?: string;
76 | tooltip: React.ReactNode;
77 | children: React.ReactNode;
78 | side?: "top" | "bottom" | "left" | "right";
79 | } & React.ComponentProps;
80 |
81 | const MessageAction = ({
82 | tooltip,
83 | children,
84 | className,
85 | side = "top",
86 | ...props
87 | }: MessageActionProps) => {
88 | return (
89 |
90 |
91 | {children}
92 |
93 | {tooltip}
94 |
95 |
96 |
97 | );
98 | };
99 |
100 | export { Message, MessageAvatar, MessageContent, MessageActions, MessageAction };
101 |
--------------------------------------------------------------------------------
/apps/web/src/features/chat/chat-input/file-items.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { X } from "lucide-react";
4 | import Image from "next/image";
5 | import { useState } from "react";
6 | import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
7 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
8 |
9 | type FileItemProps = {
10 | file: File;
11 | onRemove: (file: File) => void;
12 | };
13 |
14 | export function FileItem({ file, onRemove }: FileItemProps) {
15 | const [isRemoving, setIsRemoving] = useState(false);
16 | const [isOpen, setIsOpen] = useState(false);
17 |
18 | const handleRemove = () => {
19 | setIsRemoving(true);
20 | onRemove(file);
21 | };
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
29 | {file.type.includes("image") ? (
30 |
37 | ) : (
38 |
39 | {file.name.split(".").pop()?.toUpperCase()}
40 |
41 | )}
42 |
43 |
44 | {file.name}
45 | {(file.size / 1024).toFixed(2)}kB
46 |
47 |
48 |
49 |
50 |
57 |
58 |
59 | {isRemoving ? null : (
60 |
61 |
62 |
70 |
71 | Remove file
72 |
73 | )}
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/apps/web/src/features/chat/kit/prompt-suggestion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button, type buttonVariants } from "@/components/ui/button";
4 | import { cn } from "@/lib/utils";
5 | import type { VariantProps } from "class-variance-authority";
6 |
7 | export type PromptSuggestionProps = {
8 | children: React.ReactNode;
9 | variant?: VariantProps["variant"];
10 | size?: VariantProps["size"];
11 | className?: string;
12 | highlight?: string;
13 | } & React.ButtonHTMLAttributes;
14 |
15 | function PromptSuggestion({
16 | children,
17 | variant,
18 | size,
19 | className,
20 | highlight,
21 | ...props
22 | }: PromptSuggestionProps) {
23 | const isHighlightMode = highlight !== undefined && highlight.trim() !== "";
24 | const content = typeof children === "string" ? children : "";
25 |
26 | if (!isHighlightMode) {
27 | return (
28 |
36 | );
37 | }
38 |
39 | if (!content) {
40 | return (
41 |
49 | );
50 | }
51 |
52 | const trimmedHighlight = highlight.trim();
53 | const contentLower = content.toLowerCase();
54 | const highlightLower = trimmedHighlight.toLowerCase();
55 | const shouldHighlight = contentLower.includes(highlightLower);
56 |
57 | return (
58 |
91 | );
92 | }
93 |
94 | export { PromptSuggestion };
95 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | /** biome-ignore-all lint/nursery/noNestedComponentDefinitions: */
2 | "use client";
3 |
4 | import { ChevronLeft, ChevronRight } from "lucide-react";
5 | import type * as React from "react";
6 | import { DayPicker } from "react-day-picker";
7 | import { buttonVariants } from "@/components/ui/button";
8 | import { cn } from "@/lib/utils";
9 |
10 | function Calendar({
11 | className,
12 | classNames,
13 | showOutsideDays = true,
14 | ...props
15 | }: React.ComponentProps) {
16 | return (
17 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
39 | : "[&:has([aria-selected])]:rounded-md"
40 | ),
41 | day: cn(
42 | buttonVariants({ variant: "ghost" }),
43 | "size-8 p-0 font-normal aria-selected:opacity-100"
44 | ),
45 | day_range_start:
46 | "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
47 | day_range_end:
48 | "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
49 | day_selected:
50 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
51 | day_today: "bg-accent text-accent-foreground",
52 | day_outside: "day-outside text-muted-foreground aria-selected:text-muted-foreground",
53 | day_disabled: "text-muted-foreground opacity-50",
54 | day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
55 | day_hidden: "invisible",
56 | ...classNames,
57 | }}
58 | components={{
59 | IconLeft: ({ className, ...props }) => (
60 |
61 | ),
62 | IconRight: ({ className, ...props }) => (
63 |
64 | ),
65 | }}
66 | showOutsideDays={showOutsideDays}
67 | {...props}
68 | />
69 | );
70 | }
71 |
72 | export { Calendar };
73 |
--------------------------------------------------------------------------------
/apps/web/src/features/chat/kit/markdown.tsx:
--------------------------------------------------------------------------------
1 | /** biome-ignore-all lint/performance/useTopLevelRegex: Used for parsing markdown */
2 | import { marked } from "marked";
3 | import { memo, useId, useMemo } from "react";
4 | import ReactMarkdown, { type Components } from "react-markdown";
5 | import remarkBreaks from "remark-breaks";
6 | import remarkGfm from "remark-gfm";
7 | import { cn } from "@/lib/utils";
8 | import { CodeBlock, CodeBlockCode } from "./code-block";
9 |
10 | export type MarkdownProps = {
11 | children: string;
12 | id?: string;
13 | className?: string;
14 | components?: Partial;
15 | };
16 |
17 | function parseMarkdownIntoBlocks(markdown: string): string[] {
18 | const tokens = marked.lexer(markdown);
19 | return tokens.map((token) => token.raw);
20 | }
21 |
22 | function extractLanguage(className?: string): string {
23 | if (!className) return "plaintext";
24 | const match = className.match(/language-(\w+)/);
25 | return match ? match[1]! : "plaintext";
26 | }
27 |
28 | const INITIAL_COMPONENTS: Partial = {
29 | code({ className, children, ...props }) {
30 | const isInline =
31 | !props.node?.position?.start.line ||
32 | props.node?.position?.start.line === props.node?.position?.end.line;
33 |
34 | if (isInline) {
35 | return (
36 |
40 | {children}
41 |
42 | );
43 | }
44 |
45 | const language = extractLanguage(className);
46 |
47 | return (
48 |
49 |
50 |
51 | );
52 | },
53 | pre({ children }) {
54 | return <>{children}>;
55 | },
56 | };
57 |
58 | const MemoizedMarkdownBlock = memo(
59 | function MarkdownBlock({
60 | content,
61 | components = INITIAL_COMPONENTS,
62 | }: {
63 | content: string;
64 | components?: Partial;
65 | }) {
66 | return (
67 |
68 | {content}
69 |
70 | );
71 | },
72 | function propsAreEqual(prevProps, nextProps) {
73 | return prevProps.content === nextProps.content;
74 | }
75 | );
76 |
77 | MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock";
78 |
79 | function MarkdownComponent({
80 | children,
81 | id,
82 | className,
83 | components = INITIAL_COMPONENTS,
84 | }: MarkdownProps) {
85 | const generatedId = useId();
86 | const blockId = id ?? generatedId;
87 | const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children]);
88 |
89 | return (
90 |
91 | {blocks.map((block, index) => (
92 |
97 | ))}
98 |
99 | );
100 | }
101 |
102 | const Markdown = memo(MarkdownComponent);
103 | Markdown.displayName = "Markdown";
104 |
105 | export { Markdown };
106 |
--------------------------------------------------------------------------------
/apps/api/src/modules/chat/chat-stream.routes.ts:
--------------------------------------------------------------------------------
1 | import { anthropic } from "@ai-sdk/anthropic";
2 | import { google } from "@ai-sdk/google";
3 | import { newId } from "@repo/id";
4 | import { logger } from "@repo/logs";
5 | import {
6 | convertToModelMessages,
7 | createUIMessageStream,
8 | JsonToSseTransformStream,
9 | smoothStream,
10 | stepCountIs,
11 | streamText,
12 | type UIMessage,
13 | } from "ai";
14 | import { Hono } from "hono";
15 | import { stream } from "hono/streaming";
16 | import { chatService } from "@/modules/chat/chats.service";
17 | import { weatherTool } from "@/modules/chat/tools/weather.tool";
18 | import { auth, getUserId, requireAuth } from "@/pkg/middleware/clerk-auth";
19 |
20 | export const chatRoutes = new Hono().use("*", auth(), requireAuth).post("/", async (c) => {
21 | const { message, chatId } = (await c.req.json()) as {
22 | message: UIMessage;
23 | chatId: string;
24 | };
25 |
26 | const userId = getUserId(c);
27 | const claude = anthropic("claude-4-sonnet-20250514");
28 | const googleModel = google("gemini-2.5-flash");
29 |
30 | const chat = await chatService.getOrCreateChat({
31 | userId,
32 | id: chatId,
33 | firstMessage: message.parts?.[0]?.type === "text" ? message.parts[0].text : undefined,
34 | });
35 | const messages = (await chatService.getChatMessages({ chatId, userId })) as UIMessage[];
36 |
37 | await chatService.saveMessages({
38 | chatId: chat.id,
39 | userId,
40 | messages: [message],
41 | });
42 |
43 | const modelMessages = convertToModelMessages([...messages, message]);
44 |
45 | const originalStream = createUIMessageStream({
46 | execute: ({ writer }) => {
47 | const result = streamText({
48 | headers: {
49 | "anthropic-beta": "interleaved-thinking-2025-05-14",
50 | },
51 | providerOptions: {
52 | google: {
53 | thinking: { type: "enabled", budgetTokens: 1024 },
54 | },
55 | anthropic: {
56 | thinking: { type: "enabled", budgetTokens: 1024 },
57 | },
58 | },
59 | system: "You are a helpful assistant.",
60 | tools: {
61 | weather: weatherTool(),
62 | },
63 | model: claude,
64 | messages: modelMessages,
65 | stopWhen: stepCountIs(10),
66 | experimental_transform: smoothStream({
67 | delayInMs: 20,
68 | }),
69 | });
70 |
71 | writer.merge(
72 | result.toUIMessageStream({ sendReasoning: true, generateMessageId: () => newId("message") })
73 | );
74 | },
75 | onError: (error: any) => {
76 | logger.error("Issue with chat stream", error);
77 | return "error";
78 | },
79 | onFinish: async (res) => {
80 | await chatService.saveMessages({
81 | chatId: chat.id,
82 | userId,
83 | messages: res.messages,
84 | });
85 | },
86 | });
87 |
88 | c.header("content-type", "text/event-stream");
89 | c.header("cache-control", "no-cache");
90 | c.header("connection", "keep-alive");
91 | c.header("x-vercel-ai-data-stream", "v2");
92 | c.header("x-accel-buffering", "no");
93 |
94 | return stream(c, (stream) => {
95 | return stream.pipe(originalStream.pipeThrough(new JsonToSseTransformStream()));
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | import type * as React from "react";
2 | import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react";
3 |
4 | import { cn } from "@/lib/utils";
5 | import { type Button, buttonVariants } from "@/components/ui/button";
6 |
7 | function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
8 | return (
9 | |