Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
932308d
feat(issue-79): add backend support for chat sharing
fkesheh Nov 6, 2025
cc1838f
feat(issue-79): add chat sharing UI components
fkesheh Nov 6, 2025
0d53520
feat(issue-79): integrate sharing into chat interface
fkesheh Nov 6, 2025
cca7ca0
fix(issue-79): resolve share dialog state and UUID validation issues
fkesheh Nov 6, 2025
b142abb
fix(issue-79): address PR review comments and linting issues
fkesheh Nov 7, 2025
650d528
fix(issue-79): add error handling for clipboard operations
fkesheh Nov 7, 2025
1ffd7f8
fix(issue-79): revert Stripe API version and add UUID validation
fkesheh Nov 7, 2025
fb2090a
fix(issue-79): add share_id and share_date to getChatById validator
fkesheh Nov 7, 2025
fefdd6f
fix(issue-79): add Content-Type fallback for file uploads
fkesheh Nov 7, 2025
cdfbc25
feat(issue-79): improve file/image display in shared chats
fkesheh Nov 7, 2025
5dccc26
style(issue-79): match regular chat file card style in shared chats
fkesheh Nov 7, 2025
d98c7bc
fix(issue-79): properly differentiate images from files in shared chats
fkesheh Nov 7, 2025
4188558
style(issue-79): make HackerAI branding white in shared chat footer
fkesheh Nov 7, 2025
dbc0134
feat(shared-chats): Implement syntax-highlighted code blocks
fkesheh Nov 14, 2025
67ec075
feat(shared-chat): update share button style
fkesheh Nov 14, 2025
29a7a3c
feat(shared-chat): adding computer sidebar
fkesheh Nov 14, 2025
35fadef
feat(shared-chat): removing frozen message
fkesheh Nov 14, 2025
7055b8c
feat(shared-chat): changing file and images representations
fkesheh Nov 14, 2025
0e6ae46
feat(shared-chat): redesign share dialog with auto-generation and pre…
fkesheh Nov 14, 2025
ecd857d
feat(shared-chats): move shared chats to separate management dialog
fkesheh Nov 14, 2025
924499f
refactor(shared-chats): remove icon from manage button to match other…
fkesheh Nov 14, 2025
c88e88f
feat(shared-chat): use Header component for unlogged users in shared …
fkesheh Nov 14, 2025
8746163
feat(shared-chat): add login bar, chat input, and sidebar for shared …
fkesheh Nov 14, 2025
5e9add4
feat: improve shared chat UI with dynamic title and file upload indic…
rossmanko Nov 16, 2025
7f5516b
test: remove ShareDialog tests to fix failing tests
fkesheh Nov 17, 2025
42ae0a8
test: add ShareDialog tests
fkesheh Nov 17, 2025
59f2d0e
fix: improve keyboard accessibility in ManageSharedChatsDialog
fkesheh Nov 17, 2025
c44b931
Hide sharing button on mobile chat header
rossmanko Nov 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 114 additions & 70 deletions app/components/ChatHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React from "react";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { useAuth } from "@workos-inc/authkit-nextjs/components";
import { PanelLeft, Sparkle, SquarePen, HatGlasses, Split } from "lucide-react";
Expand All @@ -14,14 +14,20 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ShareDialog } from "./ShareDialog";

interface ChatHeaderProps {
hasMessages: boolean;
hasActiveChat: boolean;
chatTitle?: string | null;
id?: string;
chatData?:
| { title?: string; branched_from_chat_id?: string }
| {
title?: string;
branched_from_chat_id?: string;
share_id?: string;
share_date?: number;
}
| null
| undefined;
chatSidebarOpen?: boolean;
Expand Down Expand Up @@ -55,6 +61,7 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
// Removed useUpgrade hook - we now redirect to pricing dialog instead
const router = useRouter();
const isMobile = useIsMobile();
const [showShareDialog, setShowShareDialog] = useState(false);

// Show sidebar toggle for logged-in users
const showSidebarToggle = user && !loading;
Expand All @@ -63,7 +70,7 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
const isInChat = isExistingChat;

// Check if this is a branched chat
const isBranchedChat = !!(chatData as any)?.branched_from_chat_id;
const isBranchedChat = !!chatData?.branched_from_chat_id;

const handleSignIn = () => {
window.location.href = "/login";
Expand Down Expand Up @@ -264,79 +271,116 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
// Show chat header when there are messages or active chat
if (hasMessages || hasActiveChat) {
return (
<div className="px-4 bg-background flex-shrink-0">
<div className="sm:min-w-[390px] flex flex-row items-center justify-between pt-3 pb-1 gap-1 sticky top-0 z-10 bg-background flex-shrink-0">
<div className="flex items-center flex-1">
<div className="relative flex items-center">
{/* Only show sidebar toggle on mobile - desktop uses collapsed sidebar logo */}
{showSidebarToggle && !chatSidebarOpen && (
<Button
variant="ghost"
size="icon"
aria-label="Open sidebar"
onClick={toggleChatSidebar}
className="h-7 w-7 md:hidden"
>
<PanelLeft className="size-5" />
</Button>
)}
<>
<ShareDialog
open={showShareDialog}
onOpenChange={setShowShareDialog}
chatId={id || ""}
chatTitle={chatTitle || ""}
existingShareId={chatData?.share_id}
existingShareDate={chatData?.share_date}
/>
<div className="px-4 bg-background flex-shrink-0">
<div className="sm:min-w-[390px] flex flex-row items-center justify-between pt-3 pb-1 gap-1 sticky top-0 z-10 bg-background flex-shrink-0">
<div className="flex items-center flex-1">
<div className="relative flex items-center">
{/* Only show sidebar toggle on mobile - desktop uses collapsed sidebar logo */}
{showSidebarToggle && !chatSidebarOpen && (
<Button
variant="ghost"
size="icon"
aria-label="Open sidebar"
onClick={toggleChatSidebar}
className="h-7 w-7 md:hidden"
>
<PanelLeft className="size-5" />
</Button>
)}
</div>
</div>
</div>
<div className="max-w-full sm:max-w-[768px] sm:min-w-[390px] flex w-full flex-col gap-[4px] overflow-hidden">
<div className="w-full flex flex-row items-center justify-between flex-1 min-w-0 gap-[24px]">
<div className="flex flex-row items-center gap-[6px] flex-1 min-w-0 text-foreground text-lg font-medium">
<span className="whitespace-nowrap text-ellipsis overflow-hidden flex items-center gap-2">
{isChatNotFound ? (
""
) : !isExistingChat && temporaryChatsEnabled ? (
<>
Temporary Chat
<HatGlasses className="size-5" />
</>
) : (
<>
{isBranchedChat && branchedFromChatTitle && (
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Split className="size-4 flex-shrink-0 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
Branched from: {branchedFromChatTitle}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{chatTitle ||
(isExistingChat && chatData === undefined
? ""
: "New Chat")}
</>
<div className="max-w-full sm:max-w-[768px] sm:min-w-[390px] flex w-full flex-col gap-[4px] overflow-hidden">
<div className="w-full flex flex-row items-center justify-between flex-1 min-w-0 gap-[24px]">
<div className="flex flex-row items-center gap-[6px] flex-1 min-w-0 text-foreground text-lg font-medium">
<span className="whitespace-nowrap text-ellipsis overflow-hidden flex items-center gap-2">
{isChatNotFound ? (
""
) : !isExistingChat && temporaryChatsEnabled ? (
<>
Temporary Chat
<HatGlasses className="size-5" />
</>
) : (
<>
{isBranchedChat && branchedFromChatTitle && (
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Split className="size-4 flex-shrink-0 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
Branched from: {branchedFromChatTitle}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{chatTitle ||
(isExistingChat && chatData === undefined
? ""
: "New Chat")}
</>
)}
</span>
</div>
{/* Share button - only show for existing chats that aren't temporary, hide on mobile */}
{isExistingChat &&
!temporaryChatsEnabled &&
id &&
chatTitle && (
<button
aria-label="Share"
data-testid="share-chat-button"
onClick={() => setShowShareDialog(true)}
className="relative mx-2 flex-shrink-0 rounded-full h-[34px] px-3 py-0 text-sm font-medium transition-colors hover:bg-[#ffffff1a] max-md:hidden"
>
<div className="flex w-full items-center justify-center gap-1.5">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
aria-label=""
className="-ms-0.5"
>
<path d="M2.66821 12.6663V12.5003C2.66821 12.1331 2.96598 11.8353 3.33325 11.8353C3.70052 11.8353 3.99829 12.1331 3.99829 12.5003V12.6663C3.99829 13.3772 3.9992 13.8707 4.03052 14.2542C4.0612 14.6298 4.11803 14.8413 4.19849 14.9993L4.2688 15.1263C4.44511 15.4137 4.69813 15.6481 5.00024 15.8021L5.13013 15.8577C5.2739 15.9092 5.46341 15.947 5.74536 15.97C6.12888 16.0014 6.62221 16.0013 7.33325 16.0013H12.6663C13.3771 16.0013 13.8707 16.0014 14.2542 15.97C14.6295 15.9394 14.8413 15.8825 14.9993 15.8021L15.1262 15.7308C15.4136 15.5545 15.6481 15.3014 15.802 14.9993L15.8577 14.8695C15.9091 14.7257 15.9469 14.536 15.97 14.2542C16.0013 13.8707 16.0012 13.3772 16.0012 12.6663V12.5003C16.0012 12.1332 16.2991 11.8355 16.6663 11.8353C17.0335 11.8353 17.3313 12.1331 17.3313 12.5003V12.6663C17.3313 13.3553 17.3319 13.9124 17.2952 14.3626C17.2624 14.7636 17.1974 15.1247 17.053 15.4613L16.9866 15.6038C16.7211 16.1248 16.3172 16.5605 15.8215 16.8646L15.6038 16.9866C15.227 17.1786 14.8206 17.2578 14.3625 17.2952C13.9123 17.332 13.3553 17.3314 12.6663 17.3314H7.33325C6.64416 17.3314 6.0872 17.332 5.63696 17.2952C5.23642 17.2625 4.87552 17.1982 4.53931 17.054L4.39673 16.9866C3.87561 16.7211 3.43911 16.3174 3.13501 15.8216L3.01294 15.6038C2.82097 15.2271 2.74177 14.8206 2.70435 14.3626C2.66758 13.9124 2.66821 13.3553 2.66821 12.6663ZM9.33521 12.5003V4.9388L7.13696 7.13704C6.87732 7.39668 6.45625 7.39657 6.19653 7.13704C5.93684 6.87734 5.93684 6.45631 6.19653 6.19661L9.52954 2.86263L9.6311 2.77962C9.73949 2.70742 9.86809 2.66829 10.0002 2.66829C10.1763 2.66838 10.3454 2.73819 10.47 2.86263L13.804 6.19661C14.0633 6.45628 14.0634 6.87744 13.804 7.13704C13.5443 7.39674 13.1222 7.39674 12.8625 7.13704L10.6653 4.93977V12.5003C10.6651 12.8673 10.3673 13.1652 10.0002 13.1654C9.63308 13.1654 9.33538 12.8674 9.33521 12.5003Z" />
</svg>
Share
</div>
</button>
)}
</span>
</div>
</div>
</div>
<div className="flex-1 flex justify-end">
{/* New Chat Button - Show on mobile when in a chat or when temporary chat is active */}
{isMobile &&
(isInChat || (!isExistingChat && temporaryChatsEnabled)) &&
showSidebarToggle && (
<Button
variant="ghost"
size="icon"
aria-label="Start new chat"
onClick={handleNewChat}
className="h-7 w-7"
>
<SquarePen className="size-5" />
</Button>
)}
<div className="flex-1 flex justify-end">
{/* New Chat Button - Show on mobile when in a chat or when temporary chat is active */}
{isMobile &&
(isInChat || (!isExistingChat && temporaryChatsEnabled)) &&
showSidebarToggle && (
<Button
variant="ghost"
size="icon"
aria-label="Start new chat"
onClick={handleNewChat}
className="h-7 w-7"
>
<SquarePen className="size-5" />
</Button>
)}
</div>
</div>
</div>
</div>
</>
);
}

Expand Down
41 changes: 41 additions & 0 deletions app/components/ChatItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,30 @@ import { Ellipsis, Trash2, Edit2, Split } from "lucide-react";
import { useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
import { removeDraft } from "@/lib/utils/client-storage";
import { ShareDialog } from "./ShareDialog";

interface ChatItemProps {
id: string;
title: string;
isBranched?: boolean;
branchedFromTitle?: string;
shareId?: string;
shareDate?: number;
}

const ChatItem: React.FC<ChatItemProps> = ({
id,
title,
isBranched = false,
branchedFromTitle,
shareId,
shareDate,
}) => {
const router = useRouter();
const [isHovered, setIsHovered] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [showRenameDialog, setShowRenameDialog] = useState(false);
const [showShareDialog, setShowShareDialog] = useState(false);
const [editTitle, setEditTitle] = useState(title);
const [isRenaming, setIsRenaming] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -144,6 +150,18 @@ const ChatItem: React.FC<ChatItemProps> = ({
}, 50);
};

const handleShare = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// Close dropdown first, then open share dialog
setIsDropdownOpen(false);

// Small delay to ensure dropdown is fully closed before opening dialog
setTimeout(() => {
setShowShareDialog(true);
}, 50);
};

const handleSaveRename = async () => {
const trimmedTitle = editTitle.trim();

Expand Down Expand Up @@ -265,6 +283,19 @@ const ChatItem: React.FC<ChatItemProps> = ({
<Edit2 className="mr-2 h-4 w-4" />
Rename
</DropdownMenuItem>
<DropdownMenuItem onClick={handleShare}>
<svg
width="16"
height="16"
viewBox="0 0 20 20"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
className="mr-2 h-4 w-4"
>
<path d="M2.66821 12.6663V12.5003C2.66821 12.1331 2.96598 11.8353 3.33325 11.8353C3.70052 11.8353 3.99829 12.1331 3.99829 12.5003V12.6663C3.99829 13.3772 3.9992 13.8707 4.03052 14.2542C4.0612 14.6298 4.11803 14.8413 4.19849 14.9993L4.2688 15.1263C4.44511 15.4137 4.69813 15.6481 5.00024 15.8021L5.13013 15.8577C5.2739 15.9092 5.46341 15.947 5.74536 15.97C6.12888 16.0014 6.62221 16.0013 7.33325 16.0013H12.6663C13.3771 16.0013 13.8707 16.0014 14.2542 15.97C14.6295 15.9394 14.8413 15.8825 14.9993 15.8021L15.1262 15.7308C15.4136 15.5545 15.6481 15.3014 15.802 14.9993L15.8577 14.8695C15.9091 14.7257 15.9469 14.536 15.97 14.2542C16.0013 13.8707 16.0012 13.3772 16.0012 12.6663V12.5003C16.0012 12.1332 16.2991 11.8355 16.6663 11.8353C17.0335 11.8353 17.3313 12.1331 17.3313 12.5003V12.6663C17.3313 13.3553 17.3319 13.9124 17.2952 14.3626C17.2624 14.7636 17.1974 15.1247 17.053 15.4613L16.9866 15.6038C16.7211 16.1248 16.3172 16.5605 15.8215 16.8646L15.6038 16.9866C15.227 17.1786 14.8206 17.2578 14.3625 17.2952C13.9123 17.332 13.3553 17.3314 12.6663 17.3314H7.33325C6.64416 17.3314 6.0872 17.332 5.63696 17.2952C5.23642 17.2625 4.87552 17.1982 4.53931 17.054L4.39673 16.9866C3.87561 16.7211 3.43911 16.3174 3.13501 15.8216L3.01294 15.6038C2.82097 15.2271 2.74177 14.8206 2.70435 14.3626C2.66758 13.9124 2.66821 13.3553 2.66821 12.6663ZM9.33521 12.5003V4.9388L7.13696 7.13704C6.87732 7.39668 6.45625 7.39657 6.19653 7.13704C5.93684 6.87734 5.93684 6.45631 6.19653 6.19661L9.52954 2.86263L9.6311 2.77962C9.73949 2.70742 9.86809 2.66829 10.0002 2.66829C10.1763 2.66838 10.3454 2.73819 10.47 2.86263L13.804 6.19661C14.0633 6.45628 14.0634 6.87744 13.804 7.13704C13.5443 7.39674 13.1222 7.39674 12.8625 7.13704L10.6653 4.93977V12.5003C10.6651 12.8673 10.3673 13.1652 10.0002 13.1654C9.63308 13.1654 9.33538 12.8674 9.33521 12.5003Z" />
</svg>
Share
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleDelete}
className="text-destructive focus:text-destructive"
Expand Down Expand Up @@ -317,6 +348,16 @@ const ChatItem: React.FC<ChatItemProps> = ({
</DialogFooter>
</DialogContent>
</Dialog>

{/* Share Dialog */}
<ShareDialog
open={showShareDialog}
onOpenChange={setShowShareDialog}
chatId={id}
chatTitle={displayTitle}
existingShareId={shareId}
existingShareDate={shareDate}
/>
</div>
);
};
Expand Down
27 changes: 25 additions & 2 deletions app/components/ComputerSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,20 @@ import {
isSidebarFile,
isSidebarTerminal,
isSidebarPython,
type SidebarContent,
} from "@/types/chat";

export const ComputerSidebar: React.FC = () => {
const { sidebarOpen, sidebarContent, closeSidebar } = useGlobalState();
interface ComputerSidebarProps {
sidebarOpen: boolean;
sidebarContent: SidebarContent | null;
closeSidebar: () => void;
}

export const ComputerSidebarBase: React.FC<ComputerSidebarProps> = ({
sidebarOpen,
sidebarContent,
closeSidebar,
}) => {
const [isWrapped, setIsWrapped] = useState(true);

if (!sidebarOpen || !sidebarContent) {
Expand Down Expand Up @@ -322,3 +332,16 @@ export const ComputerSidebar: React.FC = () => {
</div>
);
};

// Wrapper for normal chats using GlobalState
export const ComputerSidebar: React.FC = () => {
const { sidebarOpen, sidebarContent, closeSidebar } = useGlobalState();

return (
<ComputerSidebarBase
sidebarOpen={sidebarOpen}
sidebarContent={sidebarContent}
closeSidebar={closeSidebar}
/>
);
};
Loading