Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
63 changes: 21 additions & 42 deletions apps/desktop2/src/components/chat/body.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,48 @@
import type { UIMessage } from "ai";
import { MessageCircle } from "lucide-react";
import { useEffect, useRef } from "react";
import { Streamdown } from "streamdown";

import { cn } from "@hypr/ui/lib/utils";
import { useShell } from "../../contexts/shell";
import { ChatBodyMessage } from "./message";

export function ChatBody({ messages }: { messages: UIMessage[] }) {
const scrollRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
const { chat } = useShell();

useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages]);

if (messages.length === 0) {
return <ChatBodyEmpty />;
}

return (
<div ref={scrollRef} className="flex-1 overflow-y-auto">
<div className="flex flex-col">
{messages.map((message) => <ChatBodyMessage key={message.id} message={message} />)}
</div>
<div
ref={scrollRef}
className={cn([
"flex-1 overflow-y-auto",
chat.mode === "RightPanelOpen" && "border mt-1 mr-1 rounded-md rounded-b-none",
])}
>
{messages.length === 0 ? <ChatBodyEmpty /> : <ChatBodyNonEmpty messages={messages} />}
</div>
);
}

function ChatBodyEmpty() {
return (
<div className="flex-1 p-4 overflow-y-auto">
<div className="flex flex-col items-center justify-center h-full text-center">
<MessageCircle className="w-12 h-12 text-neutral-300 mb-3" />
<p className="text-neutral-600 text-sm mb-2">Ask the AI assistant about anything.</p>
<p className="text-neutral-400 text-xs">It can also do few cool stuff for you.</p>
</div>
<div className="flex flex-col items-center justify-center h-full text-center p-4">
<MessageCircle className="w-12 h-12 text-neutral-300 mb-3" />
<p className="text-neutral-600 text-sm mb-2">Ask the AI assistant about anything.</p>
<p className="text-neutral-400 text-xs">It can also do few cool stuff for you.</p>
</div>
);
}

function ChatBodyMessage({ message }: { message: UIMessage }) {
const isUser = message.role === "user";

const content = message.parts
.filter((p) => p.type === "text")
.map((p) => (p.type === "text" ? p.text : ""))
.join("");

function ChatBodyNonEmpty({ messages }: { messages: UIMessage[] }) {
return (
<div className={`flex ${isUser ? "justify-end" : "justify-start"} px-4 py-2`}>
<div
className={`max-w-[80%] rounded-2xl px-4 py-2 ${
isUser
? "bg-blue-500 text-white"
: "bg-neutral-100 text-neutral-900"
}`}
>
<Markdown content={content} />
</div>
<div className="flex flex-col">
{messages.map((message) => <ChatBodyMessage key={message.id} message={message} />)}
</div>
);
}

function Markdown({ content }: { content: string }) {
return (
<Streamdown className="prose prose-sm dark:prose-invert max-w-none">
{content}
</Streamdown>
);
}
45 changes: 34 additions & 11 deletions apps/desktop2/src/components/chat/header.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import clsx from "clsx";
import { formatDistanceToNow } from "date-fns";
import { ChevronDown, MessageCircle, Plus, X } from "lucide-react";
import { ChevronDown, MessageCircle, PanelRightIcon, PictureInPicture2Icon, Plus, X } from "lucide-react";
import { useState } from "react";

import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@hypr/ui/components/ui/dropdown-menu";
import { cn } from "@hypr/ui/lib/utils";
import { useShell } from "../../contexts/shell";
import * as persisted from "../../store/tinybase/persisted";

export function ChatHeader({
Expand All @@ -17,16 +18,33 @@ export function ChatHeader({
onSelectChat: (chatGroupId: string) => void;
handleClose: () => void;
}) {
return (
<div className="flex items-center justify-between px-3 py-1 border-b border-neutral-200">
<ChatGroups currentChatGroupId={currentChatGroupId} onSelectChat={onSelectChat} />
const { chat } = useShell();

<div className="flex items-center gap-0.5">
return (
<div
data-tauri-drag-region={chat.mode === "RightPanelOpen"}
className={cn([
"flex items-center justify-between px-3 py-0.5 border-b border-neutral-200",
chat.mode === "RightPanelOpen" && "border mt-1 mr-1 rounded-md",
])}
>
<div className="flex items-center">
<ChatGroups currentChatGroupId={currentChatGroupId} onSelectChat={onSelectChat} />
<ChatActionButton
icon={<Plus className="w-4 h-4" />}
onClick={onNewChat}
title="New chat"
/>
</div>

<div className="flex items-center gap-0.5">
<ChatActionButton
icon={chat.mode === "RightPanelOpen"
? <PictureInPicture2Icon className="w-4 h-4" />
: <PanelRightIcon className="w-4 h-4" />}
onClick={() => chat.sendEvent({ type: "SHIFT" })}
title="Toggle"
/>
<ChatActionButton
icon={<X className="w-4 h-4" />}
onClick={handleClose}
Expand All @@ -49,7 +67,7 @@ function ChatActionButton({
return (
<button
onClick={onClick}
className="p-2 text-neutral-600 hover:text-neutral-900 hover:bg-neutral-100 rounded-lg transition-all active:scale-95"
className="p-1 text-neutral-600 hover:text-neutral-900 hover:bg-neutral-100 rounded-lg transition-all active:scale-95"
title={title}
>
{icon}
Expand Down Expand Up @@ -90,7 +108,7 @@ function ChatGroups({
{currentChatTitle || "Ask Hyprnote anything"}
</h3>
<ChevronDown
className={clsx([
className={cn([
"w-3.5 h-3.5 text-neutral-400 transition-transform duration-200",
isDropdownOpen && "rotate-180",
])}
Expand Down Expand Up @@ -152,22 +170,27 @@ function ChatGroupItem({
return (
<button
onClick={() => onSelect(groupId)}
className={clsx([
className={cn([
"w-full text-left px-2.5 py-1.5 rounded-md transition-all group",
isActive ? "bg-neutral-100 shadow-sm" : "hover:bg-neutral-50 active:bg-neutral-100",
])}
>
<div className="flex items-center gap-2.5">
<div className="flex-shrink-0">
<MessageCircle
className={clsx([
className={cn([
"w-3.5 h-3.5 transition-colors",
isActive ? "text-neutral-700" : "text-neutral-400 group-hover:text-neutral-600",
])}
/>
</div>
<div className="flex-1 min-w-0">
<div className={clsx(["text-sm font-medium truncate", isActive ? "text-neutral-900" : "text-neutral-700"])}>
<div
className={cn([
"text-sm font-medium truncate",
isActive ? "text-neutral-900" : "text-neutral-700",
])}
>
{chatGroup.title}
</div>
<div className="text-[11px] text-neutral-500 mt-0.5">
Expand Down
21 changes: 8 additions & 13 deletions apps/desktop2/src/components/chat/index.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
import { useCallback, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useCallback } from "react";

import { commands as windowsCommands } from "@hypr/plugin-windows/v1";
import { useShell } from "../../contexts/shell";
import { useAutoCloser } from "../../hooks/useAutoCloser";
import { InteractiveContainer } from "./interactive";
import { ChatTrigger } from "./trigger";
import { ChatView } from "./view";

export function ChatFloatingButton() {
const [isOpen, setIsOpen] = useState(false);
const [chatGroupId, setChatGroupId] = useState<string | undefined>(undefined);
const { chat } = useShell();
const isOpen = chat.mode === "FloatingOpen";

useAutoCloser(() => setIsOpen(false), { esc: isOpen, outside: false });
useHotkeys("mod+j", () => setIsOpen((prev) => !prev));
useAutoCloser(() => chat.sendEvent({ type: "CLOSE" }), { esc: isOpen, outside: false });

const handleClickTrigger = useCallback(async () => {
const isExists = await windowsCommands.windowIsExists({ type: "chat" });
if (isExists) {
windowsCommands.windowDestroy({ type: "chat" });
}
setIsOpen(true);
}, []);
chat.sendEvent({ type: "OPEN" });
}, [chat]);

if (!isOpen) {
return <ChatTrigger onClick={handleClickTrigger} />;
Expand All @@ -31,11 +30,7 @@ export function ChatFloatingButton() {
width={window.innerWidth * 0.4}
height={window.innerHeight * 0.7}
>
<ChatView
chatGroupId={chatGroupId}
setChatGroupId={setChatGroupId}
onClose={() => setIsOpen(false)}
/>
<ChatView />
</InteractiveContainer>
);
}
7 changes: 4 additions & 3 deletions apps/desktop2/src/components/chat/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef } from "react";

import Editor from "@hypr/tiptap/editor";
import { cn } from "@hypr/ui/lib/utils";
import { useShell } from "../../contexts/shell";

function tiptapJsonToText(json: any): string {
if (!json || typeof json !== "object") {
Expand Down Expand Up @@ -32,6 +33,7 @@ export function ChatMessageInput({
disabled?: boolean;
}) {
const editorRef = useRef<{ editor: any }>(null);
const { chat } = useShell();

const handleSubmit = useCallback(() => {
const json = editorRef.current?.editor?.getJSON();
Expand Down Expand Up @@ -63,14 +65,13 @@ export function ChatMessageInput({

return (
<div
className={cn([
"px-3 py-2 border-t border-neutral-200",
])}
className={cn([chat.mode !== "RightPanelOpen" && "p-0.5"])}
>
<div
className={cn([
"flex items-center gap-2 px-3 py-2 border border-neutral-200 rounded-xl",
"focus-within:ring-1 focus-within:ring-blue-500 focus-within:border-blue-500",
chat.mode === "RightPanelOpen" && "rounded-t-none mb-1 mr-1 border-t-0",
])}
>
<button className={cn(["text-neutral-400 hover:text-neutral-600 transition-colors shrink-0"])}>
Expand Down
39 changes: 39 additions & 0 deletions apps/desktop2/src/components/chat/message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { UIMessage } from "ai";
import { Streamdown } from "streamdown";

import { cn } from "@hypr/ui/lib/utils";

export function ChatBodyMessage({ message }: { message: UIMessage }) {
const isUser = message.role === "user";

const content = message.parts
.filter((p) => p.type === "text")
.map((p) => (p.type === "text" ? p.text : ""))
.join("");

return (
<div
className={cn([
"flex px-4 py-2",
isUser ? "justify-end" : "justify-start",
])}
>
<div
className={cn([
"max-w-[80%] rounded-2xl px-4 py-2",
isUser ? "bg-blue-500 text-white" : "bg-neutral-100 text-neutral-900",
])}
>
<Markdown content={content} />
</div>
</div>
);
}

function Markdown({ content }: { content: string }) {
return (
<Streamdown className="prose prose-sm dark:prose-invert max-w-none">
{content}
</Streamdown>
);
}
6 changes: 1 addition & 5 deletions apps/desktop2/src/components/chat/trigger.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { cn } from "@hypr/ui/lib/utils";

export function ChatTrigger({
onClick,
}: {
onClick: () => void;
}) {
export function ChatTrigger({ onClick }: { onClick: () => void }) {
return (
<button
onClick={onClick}
Expand Down
Loading
Loading