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
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,4 @@ internal
.turbo

.windsurfrules
CLAUDE.md
.cursor/
CLAUDE.md
16 changes: 0 additions & 16 deletions apps/desktop/src/components/editor-area/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { TemplateService } from "@/utils/template-service";
import { commands as analyticsCommands } from "@hypr/plugin-analytics";
import { commands as connectorCommands } from "@hypr/plugin-connector";
import { commands as dbCommands } from "@hypr/plugin-db";
import { events as localLlmEvents } from "@hypr/plugin-local-llm";
import { commands as miscCommands } from "@hypr/plugin-misc";
import { commands as templateCommands, type Grammar } from "@hypr/plugin-template";
import Editor, { type TiptapEditor } from "@hypr/tiptap/editor";
Expand Down Expand Up @@ -362,21 +361,6 @@ export function useEnhanceMutation({
const [isCancelled, setIsCancelled] = useState(false);
const queryClient = useQueryClient();

useEffect(() => {
let unlisten: () => void;
localLlmEvents.llmEvent.listen(({ payload }) => {
if (payload.progress) {
setProgress(payload.progress);
}
}).then((fn) => {
unlisten = fn;
});

return () => {
unlisten();
};
}, []);

// Extract H1 headers at component level (always available)
const extractH1Headers = useCallback((htmlContent: string): string[] => {
if (!htmlContent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export function ChatInput(

const traverseNode = (node: any) => {
if (node.type === "mention" || node.type === "mention-@") {
if (node.attrs && node.attrs.type !== "selection") {
if (node.attrs) {
mentions.push({
id: node.attrs.id || node.attrs["data-id"],
type: node.attrs.type || node.attrs["data-type"] || "note",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { MessageContent } from "./message-content";
import { Message } from "./types";

interface ChatMessageProps {
message: Message;
sessionTitle?: string;
hasEnhancedNote?: boolean;
onApplyMarkdown?: (markdownContent: string) => void;
}

export function ChatMessage({ message, sessionTitle, hasEnhancedNote, onApplyMarkdown }: ChatMessageProps) {
if (message.isUser) {
return (
<div className="w-full mb-4 flex justify-end">
<div className="max-w-[80%]">
<div className="border border-input rounded-lg overflow-clip bg-white">
<div className="px-3 py-2">
<MessageContent
message={message}
sessionTitle={sessionTitle}
hasEnhancedNote={hasEnhancedNote}
onApplyMarkdown={onApplyMarkdown}
/>
</div>
</div>
{/* Timestamp below the message */}
<div className="text-xs text-neutral-500 mt-1 text-right">
{message.timestamp.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</div>
</div>
</div>
);
}

return (
<div className="w-full mb-4">
<MessageContent
message={message}
sessionTitle={sessionTitle}
hasEnhancedNote={hasEnhancedNote}
onApplyMarkdown={onApplyMarkdown}
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import type { UIMessage } from "@hypr/utils/ai";
import { useEffect, useRef, useState } from "react";
import { UIMessageComponent } from "./ui-message";
import { ChatMessage } from "./chat-message";
import { Message } from "./types";

interface ChatMessagesViewProps {
messages: UIMessage[];
messages: Message[];
sessionTitle?: string;
hasEnhancedNote?: boolean;
onApplyMarkdown?: (markdownContent: string) => void;
isSubmitted?: boolean;
isStreaming?: boolean;
isReady?: boolean;
isError?: boolean;
isGenerating?: boolean;
isStreamingText?: boolean;
}

function ThinkingIndicator() {
Expand All @@ -32,7 +30,7 @@ function ThinkingIndicator() {
}
`}
</style>
<div style={{ color: "rgb(115 115 115)", fontSize: "0.875rem", padding: "0 0 8px 0" }}>
<div style={{ color: "rgb(115 115 115)", fontSize: "0.875rem", padding: "4px 0" }}>
<span>Thinking</span>
<span className="thinking-dot">.</span>
<span className="thinking-dot">.</span>
Expand All @@ -43,45 +41,27 @@ function ThinkingIndicator() {
}

export function ChatMessagesView(
{ messages, sessionTitle, hasEnhancedNote, onApplyMarkdown, isSubmitted, isStreaming, isReady, isError }:
ChatMessagesViewProps,
{ messages, sessionTitle, hasEnhancedNote, onApplyMarkdown, isGenerating, isStreamingText }: ChatMessagesViewProps,
) {
const messagesEndRef = useRef<HTMLDivElement>(null);
const [showThinking, setShowThinking] = useState(false);
const thinkingTimeoutRef = useRef<NodeJS.Timeout | null>(null);

const shouldShowThinking = () => {
// Show thinking when request is submitted but not yet streaming
if (isSubmitted) {
if (!isGenerating) {
return false;
}

if (messages.length === 0) {
return true;
}

// Check if we're in transition between parts (text → tool or tool → text)
if (isStreaming && messages.length > 0) {
const lastMessage = messages[messages.length - 1];
if (lastMessage.role === "assistant" && lastMessage.parts) {
const lastPart = lastMessage.parts[lastMessage.parts.length - 1];

// Text part finished but still streaming (tool coming)
if (lastPart?.type === "text" && !(lastPart as any).state) {
return true;
}

// Tool finished but still streaming (more text/tools coming)
if (lastPart?.type?.startsWith("tool-") || lastPart?.type === "dynamic-tool") {
const toolPart = lastPart as any;
if (
toolPart.state === "output-available"
|| toolPart.state === "output-error"
) {
return true;
}
}
}
const lastMessage = messages[messages.length - 1];
if (lastMessage.isUser) {
return true;
}

// Fallback for other transition states
if (!isReady && !isStreaming && !isError) {
if (!lastMessage.isUser && !isStreamingText) {
return true;
}

Expand Down Expand Up @@ -109,16 +89,16 @@ export function ChatMessagesView(
clearTimeout(thinkingTimeoutRef.current);
}
};
}, [isSubmitted, isStreaming, isReady, isError, messages]);
}, [isGenerating, isStreamingText, messages]);

useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, showThinking]);

return (
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-6 select-text">
<div className="flex-1 overflow-y-auto p-4 space-y-4 select-text">
{messages.map((message) => (
<UIMessageComponent
<ChatMessage
key={message.id}
message={message}
sessionTitle={sessionTitle}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export * from "./chat-history-item";
export * from "./chat-history-view";
export * from "./chat-input";
export * from "./chat-message";
export * from "./chat-messages-view";
export * from "./empty-chat-state";
export * from "./floating-action-buttons";
export { MarkdownCard } from "./markdown-card";
export { MessageContent } from "./message-content";
export * from "./types";
export { UIMessageComponent } from "./ui-message";
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export function MarkdownCard(
</style>

{/* Flat card with no shadow */}
<div className="border border-neutral-200 rounded-lg bg-white overflow-hidden">
<div className="mt-4 mb-4 border border-neutral-200 rounded-lg bg-white overflow-hidden">
{/* Grey header section - Made thinner with py-1 */}
<div className="bg-neutral-50 px-4 py-1 border-b border-neutral-200 flex items-center justify-between">
<div className="text-sm text-neutral-600 flex items-center gap-2">
Expand Down
Loading
Loading