diff --git a/apps/desktop/src/components/right-panel/components/chat/message-content.tsx b/apps/desktop/src/components/right-panel/components/chat/message-content.tsx
new file mode 100644
index 0000000000..2b11a66fed
--- /dev/null
+++ b/apps/desktop/src/components/right-panel/components/chat/message-content.tsx
@@ -0,0 +1,357 @@
+import { commands as miscCommands } from "@hypr/plugin-misc";
+import Renderer from "@hypr/tiptap/renderer";
+import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@hypr/ui/components/ui/accordion";
+import { PencilRuler } from "lucide-react";
+import { useEffect, useState } from "react";
+import { MarkdownCard } from "./markdown-card";
+import { Message } from "./types";
+
+interface MessageContentProps {
+ message: Message;
+ sessionTitle?: string;
+ hasEnhancedNote?: boolean;
+ onApplyMarkdown?: (markdownContent: string) => void;
+}
+
+function ToolDetailsRenderer({ details }: { details: any }) {
+ if (!details) {
+ return (
+
+ No details available...
+
+ );
+ }
+
+ return (
+
+
+ {typeof details === 'object' ? JSON.stringify(details, null, 2) : String(details)}
+
+
+ );
+}
+
+function MarkdownText({ content, htmlContent }: { content: string; htmlContent?: string }) {
+ const [displayHtml, setDisplayHtml] = useState
("");
+
+ useEffect(() => {
+ const processContent = async () => {
+ // If we have HTML content with mentions, use it directly
+ if (htmlContent) {
+ setDisplayHtml(htmlContent);
+ return;
+ }
+
+ // Otherwise, convert markdown as usual
+ try {
+ let html = await miscCommands.opinionatedMdToHtml(content);
+
+ html = html
+ .replace(/\s*<\/p>/g, "")
+ .replace(/
\u00A0<\/p>/g, "")
+ .replace(/
<\/p>/g, "")
+ .replace(/
\s+<\/p>/g, "")
+ .replace(/
<\/p>/g, "")
+ .trim();
+
+ setDisplayHtml(html);
+ } catch (error) {
+ console.error("Failed to convert markdown:", error);
+ setDisplayHtml(content);
+ }
+ };
+
+ if (content.trim() || htmlContent) {
+ processContent();
+ }
+ }, [content, htmlContent]);
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export function MessageContent({ message, sessionTitle, hasEnhancedNote, onApplyMarkdown }: MessageContentProps) {
+ let htmlContent: string | undefined;
+ if (message.isUser && message.toolDetails) {
+ try {
+ const details = typeof message.toolDetails === "string"
+ ? JSON.parse(message.toolDetails)
+ : message.toolDetails;
+ htmlContent = details.htmlContent;
+ } catch (error) {
+ console.error("Failed to parse HTML content from toolDetails:", error);
+ }
+ }
+ if (message.type === "tool-start") {
+ const hasToolDetails = message.toolDetails;
+
+ if (hasToolDetails) {
+ return (
+
+
+
+
+
+
+
+ Called tool: {message.content}
+
+
+
+
+
+
+
+
+
+ );
+ } else {
+ return (
+
+
+
+
+ Called tool: {message.content}
+
+
+
+ );
+ }
+ }
+
+ if (message.type === "tool-result") {
+ return (
+
+
+
+
+ {message.content}
+
+
+
+ );
+ }
+
+ if (message.type === "tool-error") {
+ return (
+
+
+
+
+ Tool Error: {message.content}
+
+
+
+ );
+ }
+
+ if (!message.parts || message.parts.length === 0) {
+ return ;
+ }
+
+ return (
+
+ {message.parts.map((part, index) => (
+
+ {part.type === "text"
+ ?
+ : (
+
+ )}
+
+ ))}
+
+ );
+}
diff --git a/apps/desktop/src/components/right-panel/components/chat/ui-message.tsx b/apps/desktop/src/components/right-panel/components/chat/ui-message.tsx
deleted file mode 100644
index 7c43ecbfe4..0000000000
--- a/apps/desktop/src/components/right-panel/components/chat/ui-message.tsx
+++ /dev/null
@@ -1,473 +0,0 @@
-import { commands as miscCommands } from "@hypr/plugin-misc";
-import Renderer from "@hypr/tiptap/renderer";
-import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@hypr/ui/components/ui/accordion";
-import type { UIMessage } from "@hypr/utils/ai";
-import { AlertCircle, Check, Loader2 } from "lucide-react";
-import { type FC, useEffect, useState } from "react";
-import { parseMarkdownBlocks } from "../../utils/markdown-parser";
-import { MarkdownCard } from "./markdown-card";
-
-interface UIMessageComponentProps {
- message: UIMessage;
- sessionTitle?: string;
- hasEnhancedNote?: boolean;
- onApplyMarkdown?: (content: string) => void;
-}
-
-// Component for rendering markdown/HTML content
-const TextContent: FC<{ content: string; isHtml?: boolean }> = ({ content, isHtml }) => {
- const [displayHtml, setDisplayHtml] = useState("");
-
- useEffect(() => {
- const processContent = async () => {
- if (isHtml) {
- setDisplayHtml(content);
- return;
- }
-
- // Convert markdown to HTML
- try {
- let html = await miscCommands.opinionatedMdToHtml(content);
-
- // Clean up empty paragraphs like reference code
- html = html
- .replace(/\s*<\/p>/g, "")
- .replace(/
\u00A0<\/p>/g, "")
- .replace(/
<\/p>/g, "")
- .replace(/
\s+<\/p>/g, "")
- .replace(/
<\/p>/g, "")
- .trim();
-
- setDisplayHtml(html);
- } catch (error) {
- console.error("Failed to convert markdown:", error);
- setDisplayHtml(content);
- }
- };
-
- if (content.trim()) {
- processContent();
- }
- }, [content, isHtml]);
-
- return (
- <>
-
-
-
-
- >
- );
-};
-
-export const UIMessageComponent: FC = ({
- message,
- sessionTitle,
- hasEnhancedNote,
- onApplyMarkdown,
-}) => {
- const isUser = message.role === "user";
-
- // Extract text content from parts
- const getTextContent = () => {
- if (!message.parts || message.parts.length === 0) {
- return "";
- }
-
- const textParts = message.parts
- .filter(part => part.type === "text")
- .map(part => part.text || "")
- .join("");
-
- return textParts;
- };
-
- // User message styling
- if (isUser) {
- // Check for HTML content in metadata (for mentions/selections)
- const htmlContent = (message.metadata as any)?.htmlContent;
- const textContent = getTextContent();
-
- return (
-
-
-
- {(message as any).createdAt && (
-
- {new Date((message as any).createdAt).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- })}
-
- )}
-
-
- );
- }
-
- // Assistant message - render parts
- return (
-
- {message.parts?.map((part, index) => {
- // Text content - parse for markdown blocks
- if (part.type === "text" && part.text) {
- const parsedParts = parseMarkdownBlocks(part.text);
-
- return (
-
- {parsedParts.map((parsedPart, pIndex) => {
- if (parsedPart.type === "markdown") {
- return (
-
- );
- }
- // Regular text
- return (
-
-
-
- );
- })}
-
- );
- }
-
- // Handle tool parts - check for dynamic tools or specific tool types
- if (part.type === "dynamic-tool" || part.type?.startsWith("tool-")) {
- const toolPart = part as any;
-
- // Extract tool name - either from toolName field (dynamic) or from type (specific)
- const toolName = toolPart.toolName || part.type.replace("tool-", "");
-
- // Tool execution start (input streaming or available)
- if (
- (toolPart.state === "input-streaming" || toolPart.state === "input-available")
- ) {
- return (
-
-
-
-
-
-
-
- {toolPart.state === "input-streaming" ? "Calling" : "Called"} tool: {toolName}
-
-
-
-
- {toolPart.input && (
-
-
- Input:
-
-
- {JSON.stringify(toolPart.input, null, 2)}
-
-
- )}
-
-
-
-
- );
- }
-
- // Tool completion (output available)
- if (toolPart.state === "output-available") {
- return (
-
-
-
-
-
-
-
- Tool finished: {toolName}
-
-
-
-
- {/* Show input */}
- {toolPart.input && (
-
-
- Input:
-
-
- {JSON.stringify(toolPart.input, null, 2)}
-
-
- )}
-
- {/* Show output */}
- {toolPart.output && (
-
-
- Output:
-
-
- {JSON.stringify(toolPart.output, null, 2)}
-
-
- )}
-
-
-
-
- );
- }
-
- // Tool error
- if (toolPart.state === "output-error") {
- return (
-
-
-
-
- Tool error: {toolName}
-
-
- {toolPart.errorText && (
-
- {toolPart.errorText}
-
- )}
-
- );
- }
- }
-
- return null;
- })}
-
- );
-};
diff --git a/apps/desktop/src/components/right-panel/hooks/useChat2.ts b/apps/desktop/src/components/right-panel/hooks/useChat2.ts
deleted file mode 100644
index 875cae26ca..0000000000
--- a/apps/desktop/src/components/right-panel/hooks/useChat2.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-import { useLicense } from "@/hooks/use-license";
-import { commands as dbCommands } from "@hypr/plugin-db";
-import { useChat } from "@hypr/utils/ai";
-import { useCallback, useEffect, useRef } from "react";
-import { CustomChatTransport } from "../utils/chat-transport";
-
-interface UseChat2Props {
- sessionId: string | null;
- userId: string | null;
- conversationId: string | null;
- sessionData?: any;
- selectionData?: any;
- sessions?: any;
- onError?: (error: Error) => void;
-}
-
-export function useChat2({
- sessionId,
- userId,
- conversationId,
- sessionData,
- selectionData,
- sessions,
- onError,
-}: UseChat2Props) {
- const { getLicense } = useLicense();
- const transportRef = useRef(null);
- const conversationIdRef = useRef(conversationId);
-
- useEffect(() => {
- conversationIdRef.current = conversationId;
- }, [conversationId]);
-
- if (!transportRef.current) {
- transportRef.current = new CustomChatTransport({
- sessionId,
- userId,
- sessionData,
- selectionData,
- sessions,
- getLicense: getLicense as any,
- });
- }
-
- useEffect(() => {
- if (transportRef.current) {
- transportRef.current.updateOptions({
- sessionId,
- userId,
- sessionData,
- selectionData,
- sessions,
- getLicense: getLicense as any,
- });
- }
- }, [sessionId, userId, sessionData, selectionData, sessions, getLicense]);
-
- useEffect(() => {
- return () => {
- if (transportRef.current) {
- transportRef.current.cleanup();
- }
- };
- }, []);
-
- const {
- messages,
- sendMessage: sendAIMessage,
- stop,
- status,
- error,
- addToolResult,
- setMessages,
- } = useChat({
- transport: transportRef.current,
- messages: [],
- id: sessionId || "default",
- onError: async (err: any) => {
- const errorMessage = {
- id: crypto.randomUUID(),
- role: "assistant" as const,
- parts: [{
- type: "text" as const,
- text: `An error occurred: ${err.message}`,
- }] as any,
- metadata: {
- isError: true,
- errorDetails: err,
- },
- } as const;
- setMessages((prev: any) => [...prev, errorMessage]);
- stop();
- onError?.(err);
- },
- onFinish: async ({ message }: { message: any }) => {
- const currentConvId = conversationIdRef.current;
- if (currentConvId && message && message.role === "assistant") {
- try {
- await dbCommands.createMessageV2({
- id: message.id,
- conversation_id: currentConvId,
- role: "assistant" as any,
- parts: JSON.stringify(message.parts || []),
- metadata: JSON.stringify(message.metadata || {}),
- created_at: new Date().toISOString(),
- updated_at: new Date().toISOString(),
- });
- } catch (error) {
- console.error("Failed to save assistant message:", error);
- }
- } else {
- console.warn("Skipping save - missing data:", { conversationId: currentConvId, messageRole: message?.role });
- }
- },
- });
-
- const sendMessage = useCallback(
- async (
- content: string,
- options?: {
- mentionedContent?: Array<{ id: string; type: string; label: string }>;
- selectionData?: any;
- htmlContent?: string;
- conversationId?: string;
- },
- ) => {
- const metadata = {
- mentions: options?.mentionedContent,
- selectionData: options?.selectionData,
- htmlContent: options?.htmlContent,
- };
-
- const convId = options?.conversationId || conversationId;
-
- if (!convId || !content.trim()) {
- return;
- }
-
- if (transportRef.current) {
- transportRef.current.updateOptions({
- mentionedContent: options?.mentionedContent,
- selectionData: options?.selectionData,
- sessions: sessions || {},
- });
- }
-
- // Small delay to ensure options are updated before tools are loaded
- await new Promise(resolve => setTimeout(resolve, 10));
-
- try {
- const userMessageId = crypto.randomUUID();
- await dbCommands.createMessageV2({
- id: userMessageId,
- conversation_id: convId,
- role: "user" as any,
- parts: JSON.stringify([{ type: "text", text: content }]),
- metadata: JSON.stringify(metadata),
- created_at: new Date().toISOString(),
- updated_at: new Date().toISOString(),
- });
-
- sendAIMessage({
- id: userMessageId,
- role: "user",
- parts: [{ type: "text", text: content }],
- metadata,
- });
- } catch (error) {
- console.error("Failed to send message:", error);
- onError?.(error as Error);
- }
- },
- [sendAIMessage, conversationId],
- );
-
- const updateMessageParts = useCallback(
- async (messageId: string, parts: any[]) => {
- if (conversationId) {
- try {
- await dbCommands.updateMessageV2Parts(
- messageId,
- JSON.stringify(parts),
- );
- } catch (error) {
- console.error("Failed to update message parts:", error);
- }
- }
- },
- [conversationId],
- );
-
- return {
- messages,
- stop,
- setMessages,
- isGenerating: status === "streaming" || status === "submitted",
- error,
- addToolResult,
- sendMessage,
- updateMessageParts,
- status,
- };
-}
diff --git a/apps/desktop/src/components/right-panel/hooks/useChatLogic.ts b/apps/desktop/src/components/right-panel/hooks/useChatLogic.ts
new file mode 100644
index 0000000000..8759f6c4d0
--- /dev/null
+++ b/apps/desktop/src/components/right-panel/hooks/useChatLogic.ts
@@ -0,0 +1,622 @@
+import { showProGateModal } from "@/components/pro-gate-modal/service";
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
+import { useCallback, useEffect, useRef, useState } from "react";
+
+import type { SelectionData } from "@/contexts/right-panel";
+
+import { useLicense } from "@/hooks/use-license";
+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 { commands as mcpCommands } from "@hypr/plugin-mcp";
+import { commands as miscCommands } from "@hypr/plugin-misc";
+import { fetch as tauriFetch } from "@hypr/utils";
+import {
+ dynamicTool,
+ experimental_createMCPClient,
+ modelProvider,
+ smoothStream,
+ stepCountIs,
+ streamText,
+ tool,
+} from "@hypr/utils/ai";
+import { useSessions } from "@hypr/utils/contexts";
+import { useQueryClient } from "@tanstack/react-query";
+import { getLicenseKey } from "tauri-plugin-keygen-api";
+import { z } from "zod";
+import type { ActiveEntityInfo, Message } from "../types/chat-types";
+import { prepareMessageHistory } from "../utils/chat-utils";
+import { parseMarkdownBlocks } from "../utils/markdown-parser";
+import { buildVercelToolsFromMcp } from "../utils/mcp-http-wrapper";
+import { createEditEnhancedNoteTool } from "../utils/tools/edit_enhanced_note";
+import { createSearchSessionDateRangeTool } from "../utils/tools/search_session_date_range";
+import { createSearchSessionTool } from "../utils/tools/search_session_multi_keywords";
+
+interface UseChatLogicProps {
+ sessionId: string | null;
+ userId: string | null;
+ activeEntity: ActiveEntityInfo | null;
+ messages: Message[];
+ inputValue: string;
+ hasChatStarted: boolean;
+ setMessages: (messages: Message[] | ((prev: Message[]) => Message[])) => void;
+ setInputValue: (value: string) => void;
+ setHasChatStarted: (started: boolean) => void;
+ getChatGroupId: () => Promise;
+ sessionData: any;
+ chatInputRef: React.RefObject;
+ llmConnectionQuery: any;
+}
+
+export function useChatLogic({
+ sessionId,
+ userId,
+ activeEntity,
+ messages,
+ inputValue,
+ hasChatStarted,
+ setMessages,
+ setInputValue,
+ setHasChatStarted,
+ getChatGroupId,
+ sessionData,
+ chatInputRef,
+ llmConnectionQuery,
+}: UseChatLogicProps) {
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [isStreamingText, setIsStreamingText] = useState(false);
+ const isGeneratingRef = useRef(false);
+ const abortControllerRef = useRef(null);
+ const sessions = useSessions((state) => state.sessions);
+ const { getLicense } = useLicense();
+ const queryClient = useQueryClient();
+
+ // Reset generation state and abort ongoing streams when session changes
+ useEffect(() => {
+ // Abort any ongoing generation when session changes
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ abortControllerRef.current = null;
+ }
+
+ // Reset generation state for new session
+ setIsGenerating(false);
+ setIsStreamingText(false);
+ isGeneratingRef.current = false;
+ }, [sessionId]);
+
+ const handleApplyMarkdown = async (markdownContent: string) => {
+ if (!sessionId) {
+ console.error("No session ID available");
+ return;
+ }
+
+ const sessionStore = sessions[sessionId];
+ if (!sessionStore) {
+ console.error("Session not found in store");
+ return;
+ }
+
+ try {
+ const html = await miscCommands.opinionatedMdToHtml(markdownContent);
+
+ const { session, showRaw } = sessionStore.getState();
+
+ const hasEnhancedNote = !!session.enhanced_memo_html;
+
+ if (!hasEnhancedNote) {
+ sessionStore.getState().updateRawNote(html);
+ } else {
+ if (showRaw) {
+ sessionStore.getState().updateRawNote(html);
+ } else {
+ sessionStore.getState().updateEnhancedNote(html);
+ }
+ }
+ } catch (error) {
+ console.error("Failed to apply markdown content:", error);
+ }
+ };
+
+ const processUserMessage = async (
+ content: string,
+ analyticsEvent: string,
+ mentionedContent?: Array<{ id: string; type: string; label: string }>,
+ selectionData?: SelectionData,
+ htmlContent?: string,
+ ) => {
+ if (!content.trim() || isGenerating) {
+ return;
+ }
+
+ const userMessageCount = messages.filter(msg => msg.isUser).length;
+
+ if (userMessageCount >= 4 && !getLicense.data?.valid) {
+ if (userId) {
+ await analyticsCommands.event({
+ event: "pro_license_required_chat",
+ distinct_id: userId,
+ });
+ }
+ await showProGateModal("chat");
+ return;
+ }
+
+ if (userId) {
+ await analyticsCommands.event({
+ event: analyticsEvent,
+ distinct_id: userId,
+ });
+ }
+
+ if (!hasChatStarted && activeEntity) {
+ setHasChatStarted(true);
+ }
+
+ setIsGenerating(true);
+ isGeneratingRef.current = true;
+
+ const groupId = await getChatGroupId();
+
+ // Prepare toolDetails before creating the message
+ let toolDetails = null;
+ if (htmlContent && (mentionedContent?.length || selectionData)) {
+ toolDetails = { htmlContent };
+ }
+
+ const userMessage: Message = {
+ id: crypto.randomUUID(),
+ content: content,
+ isUser: true,
+ timestamp: new Date(),
+ type: "text-delta",
+ toolDetails: toolDetails, // Include toolDetails in the message object
+ };
+
+ setMessages((prev) => [...prev, userMessage]);
+ setInputValue("");
+
+ await dbCommands.upsertChatMessage({
+ id: userMessage.id,
+ group_id: groupId,
+ created_at: userMessage.timestamp.toISOString(),
+ role: "User",
+ content: userMessage.content.trim(),
+ type: "text-delta",
+ tool_details: toolDetails ? JSON.stringify(toolDetails) : null,
+ });
+
+ const aiMessageId = crypto.randomUUID();
+
+ try {
+ const provider = await modelProvider();
+ const model = provider.languageModel("defaultModel");
+
+ await queryClient.invalidateQueries({ queryKey: ["llm-connection"] });
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ const llmConnection = await connectorCommands.getLlmConnection();
+ const { type } = llmConnection;
+ const apiBase = llmConnection.connection?.api_base;
+
+ let newMcpTools: Record = {};
+ let hyprMcpTools: Record = {};
+ let mcpToolsArray: any[] = [];
+ const allMcpClients: any[] = [];
+ let hyprMcpClient: Client | null = null;
+
+ const shouldUseTools = model.modelId === "gpt-4.1" || model.modelId === "openai/gpt-4.1"
+ || model.modelId === "anthropic/claude-sonnet-4"
+ || model.modelId === "openai/gpt-4o"
+ || model.modelId === "gpt-4o"
+ || apiBase?.includes("pro.hyprnote.com")
+ || model.modelId === "openai/gpt-5";
+
+ if (shouldUseTools) {
+ const mcpServers = await mcpCommands.getServers();
+ const enabledSevers = mcpServers.filter((server) => server.enabled);
+
+ if (apiBase?.includes("pro.hyprnote.com") && getLicense.data?.valid) {
+ try {
+ const licenseKey = await getLicenseKey();
+
+ const transport = new StreamableHTTPClientTransport(
+ new URL("https://pro.hyprnote.com/mcp"),
+ {
+ fetch: tauriFetch,
+ requestInit: {
+ headers: {
+ "x-hyprnote-license-key": licenseKey || "",
+ },
+ },
+ },
+ );
+ hyprMcpClient = new Client({
+ name: "hyprmcp",
+ version: "0.1.0",
+ });
+
+ await hyprMcpClient.connect(transport);
+
+ hyprMcpTools = await buildVercelToolsFromMcp(hyprMcpClient);
+ } catch (error) {
+ console.error("Error creating and adding hyprmcp client:", error);
+ }
+ }
+
+ for (const server of enabledSevers) {
+ try {
+ const mcpClient = await experimental_createMCPClient({
+ transport: {
+ type: "sse",
+ url: server.url,
+ ...(server.headerKey && server.headerValue && {
+ headers: {
+ [server.headerKey]: server.headerValue,
+ },
+ }),
+ onerror: (error) => {
+ console.log("mcp client error: ", error);
+ },
+ onclose: () => {
+ console.log("mcp client closed");
+ },
+ },
+ });
+ allMcpClients.push(mcpClient);
+
+ const tools = await mcpClient.tools();
+ for (const [toolName, tool] of Object.entries(tools as Record)) {
+ newMcpTools[toolName] = dynamicTool({
+ description: tool.description,
+ inputSchema: tool.inputSchema || z.any(),
+ execute: tool.execute,
+ });
+ }
+ } catch (error) {
+ console.error("Error creating MCP client:", error);
+ }
+ }
+
+ mcpToolsArray = Object.keys(newMcpTools).length > 0
+ ? Object.entries(newMcpTools).map(([name, tool]) => ({
+ name,
+ description: tool.description || `Tool: ${name}`,
+ inputSchema: tool.inputSchema || "No input schema provided",
+ }))
+ : [];
+
+ for (const [toolKey, tool] of Object.entries(hyprMcpTools)) {
+ mcpToolsArray.push({
+ name: toolKey,
+ description: tool.description || `Tool: ${tool.name}`,
+ inputSchema: tool.inputSchema || "No input schema provided",
+ });
+ }
+ }
+
+ // Create tools using the refactored tool factories
+ const searchTool = createSearchSessionTool(userId);
+ const editEnhancedNoteTool = createEditEnhancedNoteTool({
+ sessionId,
+ sessions,
+ selectionData,
+ });
+ const searchSessionDateRangeTool = createSearchSessionDateRangeTool(userId);
+ const abortController = new AbortController();
+ abortControllerRef.current = abortController;
+
+ const baseTools = {
+ ...(selectionData && { edit_enhanced_note: editEnhancedNoteTool }),
+ search_sessions_date_range: searchSessionDateRangeTool,
+ search_sessions_multi_keywords: searchTool,
+ };
+
+ const { fullStream } = streamText({
+ model,
+ messages: await prepareMessageHistory(
+ messages,
+ content,
+ mentionedContent,
+ model.modelId,
+ mcpToolsArray,
+ sessionData,
+ sessionId,
+ userId,
+ apiBase,
+ selectionData, // Pass selectionData to prepareMessageHistory
+ ),
+ stopWhen: stepCountIs(5),
+ tools: {
+ ...(shouldUseTools && { ...hyprMcpTools, ...newMcpTools }),
+ ...(shouldUseTools && baseTools),
+ ...(type === "HyprLocal" && { progress_update: tool({ inputSchema: z.any() }) }),
+ },
+ onError: (error) => {
+ console.error("On Error Catch:", error);
+ setIsGenerating(false);
+ isGeneratingRef.current = false;
+ throw error;
+ },
+ onFinish: () => {
+ for (const client of allMcpClients) {
+ client.close();
+ }
+ // close hyprmcp client
+ hyprMcpClient?.close();
+ },
+ abortSignal: abortController.signal,
+ experimental_transform: smoothStream({
+ delayInMs: 30,
+ chunking: "word",
+ }),
+ });
+
+ let aiResponse = "";
+ let didInitializeAiResponse = false;
+ let currentAiTextMessageId: string | null = null;
+ let lastChunkType: string | null = null;
+
+ for await (const chunk of fullStream) {
+ if (lastChunkType === "text-delta" && chunk.type !== "text-delta" && chunk.type !== "finish-step") {
+ setIsStreamingText(false); // Text streaming has stopped, more content coming
+
+ await new Promise(resolve => setTimeout(resolve, 50));
+ }
+
+ if (chunk.type === "text-delta") {
+ setIsStreamingText(true);
+
+ setMessages((prev) => {
+ const lastMessage = prev[prev.length - 1];
+
+ if (didInitializeAiResponse && lastMessage && lastMessage.type === "text-delta") {
+ // Same type (text) -> update existing message
+
+ aiResponse += chunk.text;
+ currentAiTextMessageId = lastMessage.id;
+ const parts = parseMarkdownBlocks(aiResponse);
+
+ return prev.map(msg =>
+ msg.id === lastMessage.id
+ ? { ...msg, content: aiResponse, parts, type: "text-delta" }
+ : msg
+ );
+ } else {
+ if (!didInitializeAiResponse) {
+ aiResponse = "";
+ didInitializeAiResponse = true;
+ }
+
+ aiResponse += chunk.text;
+ const parts = parseMarkdownBlocks(aiResponse);
+
+ // Different type -> create new message
+ const newTextMessage: Message = {
+ id: crypto.randomUUID(),
+ content: aiResponse,
+ isUser: false,
+ timestamp: new Date(),
+ type: "text-delta",
+ parts,
+ };
+
+ currentAiTextMessageId = newTextMessage.id;
+ return [...prev, newTextMessage];
+ }
+ });
+ }
+
+ if (chunk.type === "tool-call" && !(chunk.toolName === "progress_update" && type === "HyprLocal")) {
+ // Save accumulated AI text before processing tool
+
+ if (currentAiTextMessageId && aiResponse.trim()) {
+ const saveAiText = async () => {
+ try {
+ await dbCommands.upsertChatMessage({
+ id: currentAiTextMessageId!,
+ group_id: groupId,
+ created_at: new Date().toISOString(),
+ role: "Assistant",
+ type: "text-delta",
+ content: aiResponse.trim(),
+ tool_details: null,
+ });
+ } catch (error) {
+ console.error("Failed to save AI text:", error);
+ }
+ };
+ saveAiText();
+ currentAiTextMessageId = null; // Reset
+ }
+
+ didInitializeAiResponse = false;
+
+ const toolStartMessage: Message = {
+ id: crypto.randomUUID(),
+ content: `${chunk.toolName}`,
+ isUser: false,
+ timestamp: new Date(),
+ type: "tool-start",
+ toolDetails: chunk.input,
+ };
+ setMessages((prev) => [...prev, toolStartMessage]);
+
+ // save message to db right away
+ await dbCommands.upsertChatMessage({
+ id: toolStartMessage.id,
+ group_id: groupId,
+ created_at: toolStartMessage.timestamp.toISOString(),
+ role: "Assistant",
+ content: toolStartMessage.content,
+ type: "tool-start",
+ tool_details: JSON.stringify(chunk.input),
+ });
+
+ // log if user is using tools in chat
+ analyticsCommands.event({
+ event: "chat_tool_call",
+ distinct_id: userId || "",
+ });
+ }
+
+ if (chunk.type === "tool-result" && !(chunk.toolName === "progress_update" && type === "HyprLocal")) {
+ didInitializeAiResponse = false;
+
+ const toolResultMessage: Message = {
+ id: crypto.randomUUID(),
+ content: `Tool finished: ${chunk.toolName}`,
+ isUser: false,
+ timestamp: new Date(),
+ type: "tool-result",
+ };
+
+ setMessages((prev) => [...prev, toolResultMessage]);
+
+ await dbCommands.upsertChatMessage({
+ id: toolResultMessage.id,
+ group_id: groupId,
+ created_at: toolResultMessage.timestamp.toISOString(),
+ role: "Assistant",
+ content: toolResultMessage.content,
+ type: "tool-result",
+ tool_details: null,
+ });
+ }
+
+ if (chunk.type === "tool-error" && !(chunk.toolName === "progress_update" && type === "HyprLocal")) {
+ didInitializeAiResponse = false;
+ const toolErrorMessage: Message = {
+ id: crypto.randomUUID(),
+ content: `Tool error: ${chunk.error}`,
+ isUser: false,
+ timestamp: new Date(),
+ type: "tool-error",
+ };
+ setMessages((prev) => [...prev, toolErrorMessage]);
+
+ await dbCommands.upsertChatMessage({
+ id: toolErrorMessage.id,
+ group_id: groupId,
+ created_at: toolErrorMessage.timestamp.toISOString(),
+ role: "Assistant",
+ content: toolErrorMessage.content,
+ type: "tool-error",
+ tool_details: null,
+ });
+ }
+
+ lastChunkType = chunk.type;
+ }
+
+ if (currentAiTextMessageId && aiResponse.trim()) {
+ await dbCommands.upsertChatMessage({
+ id: currentAiTextMessageId,
+ group_id: groupId,
+ created_at: new Date().toISOString(),
+ role: "Assistant",
+ type: "text-delta",
+ content: aiResponse.trim(),
+ tool_details: null,
+ });
+ }
+
+ setIsGenerating(false);
+ setIsStreamingText(false);
+ isGeneratingRef.current = false;
+ abortControllerRef.current = null; // Clear the abort controller on successful completion
+ } catch (error) {
+ console.error(error);
+
+ let errorMsg = "Unknown error";
+ if (typeof error === "string") {
+ errorMsg = error;
+ } else if (error instanceof Error) {
+ errorMsg = error.message || error.name || "Unknown error";
+ } else if ((error as any)?.error) {
+ errorMsg = (error as any).error;
+ } else if ((error as any)?.message) {
+ errorMsg = (error as any).message;
+ }
+
+ let finalErrorMessage = "";
+
+ if (String(errorMsg).includes("too large")) {
+ finalErrorMessage =
+ "Sorry, I encountered an error. Please try again. Your transcript or meeting notes might be too large. Please try again with a smaller transcript or meeting notes."
+ + "\n\n" + errorMsg;
+ } else if (String(errorMsg).includes("Request cancelled") || String(errorMsg).includes("Request canceled")) {
+ finalErrorMessage = "Request was cancelled mid-stream. Try again with a different message.";
+ } else {
+ finalErrorMessage = "Sorry, I encountered an error. Please try again. " + "\n\n" + errorMsg;
+ }
+
+ setIsGenerating(false);
+ setIsStreamingText(false);
+ isGeneratingRef.current = false;
+ abortControllerRef.current = null; // Clear the abort controller on error
+
+ // Create error message
+ const errorMessage: Message = {
+ id: aiMessageId,
+ content: finalErrorMessage,
+ isUser: false,
+ timestamp: new Date(),
+ type: "text-delta",
+ };
+
+ setMessages((prev) => [...prev, errorMessage]);
+
+ await dbCommands.upsertChatMessage({
+ id: aiMessageId,
+ group_id: groupId,
+ created_at: new Date().toISOString(),
+ role: "Assistant",
+ content: finalErrorMessage,
+ type: "text-delta",
+ tool_details: null,
+ });
+ }
+ };
+
+ const handleSubmit = async (
+ mentionedContent?: Array<{ id: string; type: string; label: string }>,
+ selectionData?: SelectionData,
+ htmlContent?: string,
+ ) => {
+ await processUserMessage(inputValue, "chat_message_sent", mentionedContent, selectionData, htmlContent);
+ };
+
+ const handleQuickAction = async (prompt: string) => {
+ await processUserMessage(prompt, "chat_quickaction_sent", undefined, undefined);
+
+ if (chatInputRef.current) {
+ chatInputRef.current.focus();
+ }
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSubmit();
+ }
+ };
+
+ const handleStop = useCallback(() => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ abortControllerRef.current = null;
+ }
+ }, []);
+
+ return {
+ isGenerating,
+ isStreamingText,
+ handleSubmit,
+ handleQuickAction,
+ handleApplyMarkdown,
+ handleKeyDown,
+ handleStop,
+ };
+}
diff --git a/apps/desktop/src/components/right-panel/hooks/useChatQueries.ts b/apps/desktop/src/components/right-panel/hooks/useChatQueries.ts
new file mode 100644
index 0000000000..a1de3c15f7
--- /dev/null
+++ b/apps/desktop/src/components/right-panel/hooks/useChatQueries.ts
@@ -0,0 +1,175 @@
+import { useQuery } from "@tanstack/react-query";
+import { useEffect } from "react";
+
+import { commands as dbCommands } from "@hypr/plugin-db";
+import type { Message } from "../types/chat-types";
+import { parseMarkdownBlocks } from "../utils/markdown-parser";
+
+interface UseChatQueriesProps {
+ sessionId: string | null;
+ userId: string | null;
+ currentChatGroupId: string | null;
+ setCurrentChatGroupId: (id: string | null) => void;
+ setMessages: (messages: Message[]) => void;
+ setHasChatStarted: (started: boolean) => void;
+ isGenerating?: boolean;
+ prevIsGenerating?: React.MutableRefObject;
+}
+
+export function useChatQueries({
+ sessionId,
+ userId,
+ currentChatGroupId,
+ setCurrentChatGroupId,
+ setMessages,
+ setHasChatStarted,
+ isGenerating,
+ prevIsGenerating,
+}: UseChatQueriesProps) {
+ const chatGroupsQuery = useQuery({
+ enabled: !!sessionId && !!userId,
+ queryKey: ["chat-groups", sessionId],
+ queryFn: async () => {
+ if (!sessionId || !userId) {
+ return [];
+ }
+ const groups = await dbCommands.listChatGroups(sessionId);
+
+ const groupsWithFirstMessage = await Promise.all(
+ groups.map(async (group) => {
+ const messages = await dbCommands.listChatMessages(group.id);
+ const firstUserMessage = messages.find(msg => msg.role === "User");
+
+ // Find the most recent message timestamp in this group
+ const mostRecentMessageTimestamp = messages.length > 0
+ ? Math.max(...messages.map(msg => new Date(msg.created_at).getTime()))
+ : new Date(group.created_at).getTime(); // Fallback to group creation time if no messages
+
+ return {
+ ...group,
+ firstMessage: firstUserMessage?.content || "",
+ mostRecentMessageTimestamp,
+ };
+ }),
+ );
+
+ return groupsWithFirstMessage;
+ },
+ });
+
+ useEffect(() => {
+ if (chatGroupsQuery.data && chatGroupsQuery.data.length > 0) {
+ // Sort by most recent message timestamp instead of group creation time
+ const latestGroup = chatGroupsQuery.data.sort((a, b) =>
+ b.mostRecentMessageTimestamp - a.mostRecentMessageTimestamp
+ )[0];
+ setCurrentChatGroupId(latestGroup.id);
+ } else if (chatGroupsQuery.data && chatGroupsQuery.data.length === 0) {
+ // No groups exist for this session
+ setCurrentChatGroupId(null);
+ }
+ }, [chatGroupsQuery.data, sessionId, setCurrentChatGroupId]);
+
+ const chatMessagesQuery = useQuery({
+ enabled: !!currentChatGroupId,
+ queryKey: ["chat-messages", currentChatGroupId],
+ queryFn: async () => {
+ if (!currentChatGroupId) {
+ return [];
+ }
+
+ const dbMessages = await dbCommands.listChatMessages(currentChatGroupId);
+ return dbMessages.map(msg => {
+ // Parse tool_details for all messages
+ let parsedToolDetails: any = undefined;
+ if (msg.tool_details) {
+ try {
+ parsedToolDetails = JSON.parse(msg.tool_details);
+ } catch (error) {
+ console.error("Failed to parse tool_details:", msg.id, error);
+ }
+ }
+
+ return {
+ id: msg.id,
+ content: msg.content,
+ isUser: msg.role === "User",
+ timestamp: new Date(msg.created_at),
+ type: msg.type || "text-delta",
+ parts: msg.role === "Assistant" ? parseMarkdownBlocks(msg.content) : undefined,
+ toolDetails: parsedToolDetails,
+ };
+ });
+ },
+ });
+
+ useEffect(() => {
+ const justFinishedGenerating = prevIsGenerating && prevIsGenerating.current === true && isGenerating === false;
+ if (prevIsGenerating) {
+ prevIsGenerating.current = isGenerating || false;
+ }
+
+ if (chatMessagesQuery.data) {
+ if (!isGenerating && !justFinishedGenerating) {
+ // Safe to sync from database
+ setMessages(chatMessagesQuery.data);
+ setHasChatStarted(chatMessagesQuery.data.length > 0);
+ } else {
+ // Currently generating - DON'T override local state
+ console.log("Skipping DB sync - currently generating");
+ }
+ }
+ }, [chatMessagesQuery.data, isGenerating, setMessages, setHasChatStarted, prevIsGenerating]);
+
+ const sessionData = useQuery({
+ enabled: !!sessionId,
+ queryKey: ["session", "chat-context", sessionId],
+ queryFn: async () => {
+ if (!sessionId) {
+ return null;
+ }
+
+ const session = await dbCommands.getSession({ id: sessionId });
+ if (!session) {
+ return null;
+ }
+
+ return {
+ title: session.title || "",
+ rawContent: session.raw_memo_html || "",
+ enhancedContent: session.enhanced_memo_html,
+ preMeetingContent: session.pre_meeting_memo_html,
+ words: session.words || [],
+ };
+ },
+ });
+
+ const getChatGroupId = async (): Promise => {
+ if (!sessionId || !userId) {
+ throw new Error("No session or user");
+ }
+
+ if (currentChatGroupId) {
+ return currentChatGroupId;
+ }
+
+ const chatGroup = await dbCommands.createChatGroup({
+ id: crypto.randomUUID(),
+ session_id: sessionId,
+ user_id: userId,
+ name: null,
+ created_at: new Date().toISOString(),
+ });
+
+ setCurrentChatGroupId(chatGroup.id);
+ chatGroupsQuery.refetch();
+ return chatGroup.id;
+ };
+
+ return {
+ chatGroupsQuery,
+ chatMessagesQuery,
+ sessionData,
+ getChatGroupId,
+ };
+}
diff --git a/apps/desktop/src/components/right-panel/hooks/useChatQueries2.ts b/apps/desktop/src/components/right-panel/hooks/useChatQueries2.ts
deleted file mode 100644
index 539e73ff5c..0000000000
--- a/apps/desktop/src/components/right-panel/hooks/useChatQueries2.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-import { commands as dbCommands } from "@hypr/plugin-db";
-import type { UIMessage } from "@hypr/utils/ai";
-import { useQuery } from "@tanstack/react-query";
-import { useEffect } from "react";
-
-interface UseChatQueries2Props {
- sessionId: string | null;
- userId: string | null;
- currentConversationId: string | null;
- setCurrentConversationId: (id: string | null) => void;
- setMessages: (messages: UIMessage[]) => void;
- isGenerating?: boolean;
-}
-
-export function useChatQueries2({
- sessionId,
- userId,
- currentConversationId,
- setCurrentConversationId,
- setMessages,
- isGenerating,
-}: UseChatQueries2Props) {
- const conversationsQuery = useQuery({
- enabled: !!sessionId && !!userId,
- queryKey: ["conversations", sessionId],
- queryFn: async () => {
- if (!sessionId || !userId) {
- return [];
- }
- const conversations = await dbCommands.listConversations(sessionId);
-
- const conversationsWithPreview = await Promise.all(
- conversations.map(async (conv) => {
- const messages = await dbCommands.listMessagesV2(conv.id);
- const firstUserMessage = messages.find(msg => msg.role === "user");
-
- const mostRecentTimestamp = messages.length > 0
- ? Math.max(...messages.map(msg => new Date(msg.created_at).getTime()))
- : new Date(conv.created_at).getTime();
-
- return {
- ...conv,
- firstMessage: firstUserMessage ? (JSON.parse(firstUserMessage.parts)[0]?.text || "") : "",
- mostRecentTimestamp,
- };
- }),
- );
-
- return conversationsWithPreview;
- },
- });
-
- useEffect(() => {
- if (conversationsQuery.data && conversationsQuery.data.length > 0) {
- const latestConversation = conversationsQuery.data.sort((a, b) =>
- b.mostRecentTimestamp - a.mostRecentTimestamp
- )[0];
- setCurrentConversationId(latestConversation.id);
- } else if (conversationsQuery.data && conversationsQuery.data.length === 0) {
- setCurrentConversationId(null);
- }
- }, [conversationsQuery.data, sessionId, setCurrentConversationId]);
-
- const messagesQuery = useQuery({
- enabled: !!currentConversationId,
- queryKey: ["messages", currentConversationId],
- queryFn: async () => {
- if (!currentConversationId) {
- return [];
- }
-
- const dbMessages = await dbCommands.listMessagesV2(currentConversationId);
-
- const uiMessages: UIMessage[] = dbMessages.map(msg => {
- let parts = [];
- let metadata = {};
-
- try {
- parts = JSON.parse(msg.parts);
- } catch (error) {
- console.error("Failed to parse message parts:", msg.id, error);
-
- parts = [{ type: "text", text: "" }];
- }
-
- if (msg.metadata) {
- try {
- metadata = JSON.parse(msg.metadata);
- } catch (error) {
- console.error("Failed to parse message metadata:", msg.id, error);
- }
- }
-
- return {
- id: msg.id,
- role: msg.role as "user" | "assistant" | "system",
- content: parts,
- parts: parts,
- createdAt: new Date(msg.created_at),
- metadata,
- };
- });
-
- return uiMessages;
- },
- });
-
- useEffect(() => {
- if (messagesQuery.data && !isGenerating) {
- setMessages(messagesQuery.data);
- }
- }, [messagesQuery.data, isGenerating, setMessages]);
-
- const sessionDataQuery = useQuery({
- enabled: !!sessionId,
- queryKey: ["session", "chat-context", sessionId],
- queryFn: async () => {
- if (!sessionId) {
- return null;
- }
-
- const session = await dbCommands.getSession({ id: sessionId });
- if (!session) {
- return null;
- }
-
- return {
- title: session.title || "",
- rawContent: session.raw_memo_html || "",
- enhancedContent: session.enhanced_memo_html,
- preMeetingContent: session.pre_meeting_memo_html,
- words: session.words || [],
- };
- },
- });
-
- const createConversation = async (): Promise => {
- if (!sessionId || !userId) {
- throw new Error("No session or user");
- }
-
- const conversation = await dbCommands.createConversation({
- id: crypto.randomUUID(),
- session_id: sessionId,
- user_id: userId,
- name: null,
- created_at: new Date().toISOString(),
- updated_at: new Date().toISOString(),
- });
-
- setCurrentConversationId(conversation.id);
- conversationsQuery.refetch();
- return conversation.id;
- };
-
- const getOrCreateConversationId = async (): Promise => {
- if (currentConversationId) {
- return currentConversationId;
- }
- return createConversation();
- };
-
- return {
- conversations: conversationsQuery.data || [],
- conversationsLoading: conversationsQuery.isLoading,
- messages: messagesQuery.data || [],
- messagesLoading: messagesQuery.isLoading,
- sessionData: sessionDataQuery.data,
- sessionDataLoading: sessionDataQuery.isLoading,
- createConversation,
- getOrCreateConversationId,
- refetchConversations: conversationsQuery.refetch,
- refetchMessages: messagesQuery.refetch,
- };
-}
diff --git a/apps/desktop/src/components/right-panel/utils/chat-transport.ts b/apps/desktop/src/components/right-panel/utils/chat-transport.ts
deleted file mode 100644
index a70e7616bc..0000000000
--- a/apps/desktop/src/components/right-panel/utils/chat-transport.ts
+++ /dev/null
@@ -1,274 +0,0 @@
-import { commands as connectorCommands } from "@hypr/plugin-connector";
-import { commands as mcpCommands } from "@hypr/plugin-mcp";
-import { fetch as tauriFetch } from "@hypr/utils";
-import type { UIMessage } from "@hypr/utils/ai";
-import {
- type ChatRequestOptions,
- type ChatTransport,
- dynamicTool,
- experimental_createMCPClient,
- modelProvider,
- smoothStream,
- stepCountIs,
- streamText,
- type UIMessageChunk,
-} from "@hypr/utils/ai";
-import { Client } from "@modelcontextprotocol/sdk/client/index.js";
-import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
-import { getLicenseKey } from "tauri-plugin-keygen-api";
-import { z } from "zod";
-
-// Import the custom tools
-import { prepareMessagesForAI } from "./chat-utils";
-import { buildVercelToolsFromMcp } from "./mcp-http-wrapper";
-import { createEditEnhancedNoteTool } from "./tools/edit_enhanced_note";
-import { createSearchSessionDateRangeTool } from "./tools/search_session_date_range";
-import { createSearchSessionTool } from "./tools/search_session_multi_keywords";
-
-interface CustomChatTransportOptions {
- sessionId: string | null;
- userId: string | null;
- sessionData?: any;
- selectionData?: any;
- sessions?: any;
- getLicense?: { data?: { valid?: boolean } };
- mentionedContent?: Array<{ id: string; type: string; label: string }>;
-}
-
-export class CustomChatTransport implements ChatTransport {
- private options: CustomChatTransportOptions;
- private allMcpClients: any[] = [];
- private hyprMcpClient: Client | null = null;
-
- constructor(options: CustomChatTransportOptions) {
- this.options = options;
- }
-
- async initializeModel() {
- const provider = await modelProvider();
- const model = provider.languageModel("defaultModel");
-
- return model;
- }
-
- private async loadMCPTools() {
- let newMcpTools: Record = {};
- let hyprMcpTools: Record = {};
-
- const llmConnection = await connectorCommands.getLlmConnection();
- const { type } = llmConnection;
- const apiBase = llmConnection.connection?.api_base;
- const customModel = await connectorCommands.getCustomLlmModel();
-
- const modelId = type === "Custom" && customModel ? customModel : "gpt-4";
-
- const shouldUseTools = modelId === "gpt-4.1"
- || modelId === "openai/gpt-4.1"
- || modelId === "anthropic/claude-sonnet-4"
- || modelId === "openai/gpt-4o"
- || modelId === "gpt-4o"
- || apiBase?.includes("pro.hyprnote.com")
- || modelId === "openai/gpt-5";
-
- if (!shouldUseTools) {
- return { newMcpTools, hyprMcpTools };
- }
-
- const mcpServers = await mcpCommands.getServers();
- const enabledServers = mcpServers.filter((server) => server.enabled);
-
- // load Hyprnote cloud MCP if applicable
- if (apiBase?.includes("pro.hyprnote.com") && this.options.getLicense?.data?.valid) {
- try {
- const licenseKey = await getLicenseKey();
- const transport = new StreamableHTTPClientTransport(
- new URL("https://pro.hyprnote.com/mcp"),
- {
- fetch: tauriFetch,
- requestInit: {
- headers: {
- "x-hyprnote-license-key": licenseKey || "",
- },
- },
- },
- );
- this.hyprMcpClient = new Client({
- name: "hyprmcp",
- version: "0.1.0",
- });
- await this.hyprMcpClient.connect(transport);
- hyprMcpTools = await buildVercelToolsFromMcp(this.hyprMcpClient);
- } catch (error) {
- console.error("Error creating hyprmcp client:", error);
- }
- }
-
- for (const server of enabledServers) {
- try {
- const mcpClient = await experimental_createMCPClient({
- transport: {
- type: "sse",
- url: server.url,
- ...(server.headerKey && server.headerValue && {
- headers: {
- [server.headerKey]: server.headerValue,
- },
- }),
- onerror: (error: any) => console.log("mcp client error:", error),
- onclose: () => console.log("mcp client closed"),
- },
- });
- this.allMcpClients.push(mcpClient);
-
- const tools = await mcpClient.tools();
- for (const [toolName, tool] of Object.entries(tools as Record)) {
- newMcpTools[toolName] = dynamicTool({
- description: tool.description,
- inputSchema: tool.inputSchema || z.any(),
- execute: tool.execute,
- });
- }
- } catch (error) {
- console.error("Error creating MCP client:", error);
- }
- }
-
- return { newMcpTools, hyprMcpTools };
- }
-
- private async getTools() {
- const { newMcpTools, hyprMcpTools } = await this.loadMCPTools();
-
- const llmConnection = await connectorCommands.getLlmConnection();
- const { type } = llmConnection;
- const apiBase = llmConnection.connection?.api_base;
- const customModel = await connectorCommands.getCustomLlmModel();
-
- const modelId = type === "Custom" && customModel ? customModel : "gpt-4";
-
- const shouldUseTools = modelId === "gpt-4.1"
- || modelId === "openai/gpt-4.1"
- || modelId === "anthropic/claude-sonnet-4"
- || modelId === "openai/gpt-4o"
- || modelId === "gpt-4o"
- || apiBase?.includes("pro.hyprnote.com")
- || modelId === "openai/gpt-5";
-
- const searchTool = createSearchSessionTool(this.options.userId);
- const searchSessionDateRangeTool = createSearchSessionDateRangeTool(this.options.userId);
- const editEnhancedNoteTool = this.options.selectionData
- ? createEditEnhancedNoteTool({
- sessionId: this.options.sessionId,
- sessions: this.options.sessions || {},
- selectionData: this.options.selectionData,
- })
- : null;
-
- const baseTools = {
- ...(editEnhancedNoteTool && { edit_enhanced_note: editEnhancedNoteTool }),
- search_sessions_date_range: searchSessionDateRangeTool,
- search_sessions_multi_keywords: searchTool,
- };
-
- return {
- ...(shouldUseTools && { ...hyprMcpTools, ...newMcpTools }),
- ...(shouldUseTools && baseTools),
- };
- }
-
- async sendMessages(
- options: {
- chatId: string;
- messages: UIMessage[];
- abortSignal: AbortSignal | undefined;
- } & {
- trigger: "submit-message" | "regenerate-message";
- messageId: string | undefined;
- } & ChatRequestOptions,
- ): Promise> {
- try {
- const model = await this.initializeModel();
-
- const lastMessage = options.messages[options.messages.length - 1];
- const messageMetadata = lastMessage?.metadata as any;
- if (messageMetadata?.selectionData) {
- this.options.selectionData = messageMetadata.selectionData;
- }
-
- const tools = await this.getTools();
-
- const preparedMessages = await prepareMessagesForAI(options.messages, {
- sessionId: this.options.sessionId,
- userId: this.options.userId,
- sessionData: this.options.sessionData,
- selectionData: this.options.selectionData,
- mentionedContent: this.options.mentionedContent,
- });
-
- const result = streamText({
- model,
- messages: preparedMessages,
- abortSignal: options.abortSignal,
- stopWhen: stepCountIs(10),
- tools,
- toolChoice: "auto",
- experimental_transform: smoothStream({
- delayInMs: 70,
- chunking: "word",
- }),
- onFinish: () => {
- for (const client of this.allMcpClients) {
- client.close();
- }
- if (this.hyprMcpClient) {
- this.hyprMcpClient.close();
- }
- this.allMcpClients = [];
- this.hyprMcpClient = null;
- },
- });
-
- return result.toUIMessageStream({
- onError: (error) => {
- if (error == null) {
- return "unknown_error";
- }
- if (typeof error === "string") {
- return error;
- }
- if (error instanceof Error) {
- return error.message;
- }
- return JSON.stringify(error);
- },
- });
- } catch (error) {
- console.error("Transport error:", error);
- throw error;
- }
- }
-
- async reconnectToStream(
- _options: {
- chatId: string;
- } & ChatRequestOptions,
- ): Promise | null> {
- return null;
- }
-
- // helper method to update options (for selection data, session data, etc.)
- updateOptions(newOptions: Partial) {
- this.options = { ...this.options, ...newOptions };
- }
-
- cleanup() {
- for (const client of this.allMcpClients) {
- client.close();
- }
- if (this.hyprMcpClient) {
- this.hyprMcpClient.close();
- }
- this.allMcpClients = [];
- this.hyprMcpClient = null;
- }
-}
diff --git a/apps/desktop/src/components/right-panel/utils/chat-utils.ts b/apps/desktop/src/components/right-panel/utils/chat-utils.ts
index f73937d781..3fb3bb7877 100644
--- a/apps/desktop/src/components/right-panel/utils/chat-utils.ts
+++ b/apps/desktop/src/components/right-panel/utils/chat-utils.ts
@@ -2,8 +2,7 @@ import type { SelectionData } from "@/contexts/right-panel";
import { commands as connectorCommands } from "@hypr/plugin-connector";
import { commands as dbCommands } from "@hypr/plugin-db";
import { commands as templateCommands } from "@hypr/plugin-template";
-import type { UIMessage } from "@hypr/utils/ai";
-import { convertToModelMessages } from "@hypr/utils/ai";
+import { Message } from "../components/chat/types";
export const formatDate = (date: Date) => {
const now = new Date();
@@ -34,101 +33,27 @@ export const focusInput = (chatInputRef: React.RefObject) =
}
};
-/**
- * Cleans UIMessages to remove tool parts with problematic states
- * that are not compatible with model messages.
- * This is a workaround for the Vercel AI SDK v5 limitation.
- */
-export const cleanUIMessages = (messages: UIMessage[]): UIMessage[] => {
- return messages.map(message => {
- // Only process messages that have parts
- if (!message.parts || !Array.isArray(message.parts)) {
- return message;
- }
+export const prepareMessageHistory = async (
+ messages: Message[],
+ currentUserMessage?: string,
+ mentionedContent?: Array<{ id: string; type: string; label: string }>,
+ modelId?: string,
+ mcpToolsArray?: Array<{ name: string; description: string; inputSchema: string }>,
+ sessionData?: any,
+ sessionId?: string | null,
+ userId?: string | null,
+ apiBase?: string | null,
+ selectionData?: SelectionData, // Add selectionData parameter
+) => {
+ const refetchResult = await sessionData?.refetch();
+ let freshSessionData = refetchResult?.data;
- // Filter out tool parts with problematic states
- const cleanedParts = message.parts.filter(part => {
- // Check if this is a tool part (dynamic-tool or tool-*)
- if (part.type === "dynamic-tool" || part.type?.startsWith("tool-")) {
- const toolPart = part as any;
-
- // Filter out UI-specific states that cause conversion errors
- // Keep only text parts and tool parts without problematic states
- if (
- toolPart.state === "input-available"
- || toolPart.state === "output-available"
- || toolPart.state === "input-streaming"
- || toolPart.state === "output-error"
- ) {
- return false; // Remove these tool parts
- }
- }
+ const { type } = await connectorCommands.getLlmConnection();
- // Keep all other parts (text, etc.)
- return true;
- });
+ const participants = sessionId ? await dbCommands.sessionListParticipants(sessionId) : [];
- return {
- ...message,
- parts: cleanedParts,
- };
- });
-};
+ const calendarEvent = sessionId ? await dbCommands.sessionGetEvent(sessionId) : null;
-/**
- * Prepares messages for AI model with system prompt and context.
- * Works with UIMessage types from Vercel AI SDK v5.
- */
-export const prepareMessagesForAI = async (
- messages: UIMessage[],
- options: {
- sessionId: string | null;
- userId: string | null;
- sessionData?: any;
- selectionData?: SelectionData;
- mentionedContent?: Array<{ id: string; type: string; label: string }>;
- },
-) => {
- const { sessionId, userId, sessionData, selectionData, mentionedContent } = options;
-
- // sessionData is already the data object from the query, not the query itself
- // It doesn't have a refetch method - it's just the plain data
- let freshSessionData = sessionData;
-
- // If no session data and we have sessionId, fetch it directly
- if (!freshSessionData && sessionId) {
- try {
- const session = await dbCommands.getSession({ id: sessionId });
- if (session) {
- freshSessionData = {
- title: session.title || "",
- rawContent: session.raw_memo_html || "",
- enhancedContent: session.enhanced_memo_html,
- preMeetingContent: session.pre_meeting_memo_html,
- words: session.words || [],
- };
- }
- } catch (error) {
- console.error("Error fetching session data:", error);
- }
- }
-
- // Get connection info
- const llmConnection = await connectorCommands.getLlmConnection();
- const { type } = llmConnection;
- const apiBase = llmConnection.connection?.api_base;
- const customModel = await connectorCommands.getCustomLlmModel();
- const modelId = type === "Custom" && customModel ? customModel : "gpt-4";
-
- // Get participants and calendar event
- const participants = sessionId
- ? await dbCommands.sessionListParticipants(sessionId)
- : [];
- const calendarEvent = sessionId
- ? await dbCommands.sessionGetEvent(sessionId)
- : null;
-
- // Format current date/time
const currentDateTime = new Date().toLocaleString("en-US", {
year: "numeric",
month: "long",
@@ -138,14 +63,12 @@ export const prepareMessagesForAI = async (
hour12: true,
});
- // Format event info
const eventInfo = calendarEvent
- ? `${calendarEvent.name} (${calendarEvent.start_date} - ${calendarEvent.end_date}${
+ ? `${calendarEvent.name} (${calendarEvent.start_date} - ${calendarEvent.end_date})${
calendarEvent.note ? ` - ${calendarEvent.note}` : ""
- })`
+ }`
: "";
- // Determine if tools are enabled
const toolEnabled = !!(
modelId === "gpt-4.1"
|| modelId === "openai/gpt-4.1"
@@ -156,17 +79,6 @@ export const prepareMessagesForAI = async (
|| (apiBase && apiBase.includes("pro.hyprnote.com"))
);
- // Get MCP tools list for system prompt
- const mcpCommands = await import("@hypr/plugin-mcp").then(m => m.commands);
- const mcpServers = await mcpCommands.getServers();
- const enabledServers = mcpServers.filter((server) => server.enabled);
- const mcpToolsArray = enabledServers.map((server) => ({
- name: server.type, // Using type as name since that's what's available
- description: "",
- inputSchema: "{}",
- }));
-
- // Generate system message using template
const systemContent = await templateCommands.render("chat.system", {
session: freshSessionData,
words: JSON.stringify(freshSessionData?.words || []),
@@ -182,138 +94,130 @@ export const prepareMessagesForAI = async (
mcpTools: mcpToolsArray,
});
- // Clean UIMessages to remove problematic tool states before conversion
- const cleanedMessages = cleanUIMessages(messages);
+ const conversationHistory: Array<{
+ role: "system" | "user" | "assistant";
+ content: string;
+ }> = [
+ { role: "system" as const, content: systemContent },
+ ];
+
+ messages.forEach(message => {
+ conversationHistory.push({
+ role: message.isUser ? ("user" as const) : ("assistant" as const),
+ content: message.content,
+ });
+ });
- // Convert cleaned UIMessages to model messages
- const modelMessages = convertToModelMessages(cleanedMessages);
- const preparedMessages: any[] = [];
+ const processedMentions: Array<{ type: string; label: string; content: string }> = [];
- // Always add system message first
- preparedMessages.push({
- role: "system",
- content: systemContent,
- });
+ if (mentionedContent && mentionedContent.length > 0) {
+ for (const mention of mentionedContent) {
+ try {
+ if (mention.type === "note") {
+ const sessionData = await dbCommands.getSession({ id: mention.id });
- // Process all messages, enhancing the last user message if needed
- for (let i = 0; i < modelMessages.length; i++) {
- const msg = modelMessages[i];
-
- // Check if this is the last user message and we have context to add
- const isLastUserMessage = i === modelMessages.length - 1 && msg.role === "user";
-
- if (isLastUserMessage && (mentionedContent || selectionData)) {
- // Process mentions
- const processedMentions: Array<{ type: string; label: string; content: string }> = [];
-
- if (mentionedContent && mentionedContent.length > 0) {
- for (const mention of mentionedContent) {
- try {
- if (mention.type === "note") {
- const sessionData = await dbCommands.getSession({ id: mention.id });
- if (sessionData) {
- let noteContent = "";
- if (sessionData.enhanced_memo_html && sessionData.enhanced_memo_html.trim() !== "") {
- noteContent = sessionData.enhanced_memo_html;
- } else if (sessionData.raw_memo_html && sessionData.raw_memo_html.trim() !== "") {
- noteContent = sessionData.raw_memo_html;
- } else {
- continue;
- }
- processedMentions.push({
- type: "note",
- label: mention.label,
- content: noteContent,
- });
- }
+ if (sessionData) {
+ let noteContent = "";
+
+ if (sessionData.enhanced_memo_html && sessionData.enhanced_memo_html.trim() !== "") {
+ noteContent = sessionData.enhanced_memo_html;
+ } else if (sessionData.raw_memo_html && sessionData.raw_memo_html.trim() !== "") {
+ noteContent = sessionData.raw_memo_html;
+ } else {
+ continue;
}
- if (mention.type === "human") {
- const humanData = await dbCommands.getHuman(mention.id);
- if (humanData) {
- let humanContent = "";
- humanContent += "Name: " + humanData?.full_name + "\n";
- humanContent += "Email: " + humanData?.email + "\n";
- humanContent += "Job Title: " + humanData?.job_title + "\n";
- humanContent += "LinkedIn: " + humanData?.linkedin_username + "\n";
-
- // Add recent sessions for this person
- if (humanData?.full_name) {
- try {
- const participantSessions = await dbCommands.listSessions({
- type: "search",
- query: humanData.full_name,
- user_id: userId || "",
- limit: 5,
- });
-
- if (participantSessions.length > 0) {
- humanContent += "\nNotes this person participated in:\n";
- for (const session of participantSessions.slice(0, 2)) {
- const participants = await dbCommands.sessionListParticipants(session.id);
- const isParticipant = participants.some((p: any) =>
- p.full_name === humanData.full_name || p.email === humanData.email
- );
-
- if (isParticipant) {
- let briefContent = "";
- if (session.enhanced_memo_html && session.enhanced_memo_html.trim() !== "") {
- // Strip HTML tags for brief content
- briefContent = session.enhanced_memo_html.replace(/<[^>]*>/g, "").slice(0, 200) + "...";
- } else if (session.raw_memo_html && session.raw_memo_html.trim() !== "") {
- briefContent = session.raw_memo_html.replace(/<[^>]*>/g, "").slice(0, 200) + "...";
- }
- humanContent += `- "${session.title || "Untitled"}": ${briefContent}\n`;
- }
- }
+ processedMentions.push({
+ type: "note",
+ label: mention.label,
+ content: noteContent,
+ });
+ }
+ }
+
+ if (mention.type === "human") {
+ const humanData = await dbCommands.getHuman(mention.id);
+
+ let humanContent = "";
+ humanContent += "Name: " + humanData?.full_name + "\n";
+ humanContent += "Email: " + humanData?.email + "\n";
+ humanContent += "Job Title: " + humanData?.job_title + "\n";
+ humanContent += "LinkedIn: " + humanData?.linkedin_username + "\n";
+
+ if (humanData?.full_name) {
+ try {
+ const participantSessions = await dbCommands.listSessions({
+ type: "search",
+ query: humanData.full_name,
+ user_id: userId || "",
+ limit: 5,
+ });
+
+ if (participantSessions.length > 0) {
+ humanContent += "\nNotes this person participated in:\n";
+
+ for (const session of participantSessions.slice(0, 2)) {
+ const participants = await dbCommands.sessionListParticipants(session.id);
+ const isParticipant = participants.some((p: any) =>
+ p.full_name === humanData.full_name || p.email === humanData.email
+ );
+
+ if (isParticipant) {
+ let briefContent = "";
+ if (session.enhanced_memo_html && session.enhanced_memo_html.trim() !== "") {
+ const div = document.createElement("div");
+ div.innerHTML = session.enhanced_memo_html;
+ briefContent = (div.textContent || div.innerText || "").slice(0, 200) + "...";
+ } else if (session.raw_memo_html && session.raw_memo_html.trim() !== "") {
+ const div = document.createElement("div");
+ div.innerHTML = session.raw_memo_html;
+ briefContent = (div.textContent || div.innerText || "").slice(0, 200) + "...";
}
- } catch (error) {
- console.error(`Error fetching notes for person "${humanData.full_name}":`, error);
+
+ humanContent += `- "${session.title || "Untitled"}": ${briefContent}\n`;
}
}
-
- processedMentions.push({
- type: "human",
- label: mention.label,
- content: humanContent,
- });
}
+ } catch (error) {
+ console.error(`Error fetching notes for person "${humanData.full_name}":`, error);
}
- } catch (error) {
- console.error(`Error fetching content for "${mention.label}":`, error);
}
- }
- }
- // Get the original user message content
- const originalContent = typeof msg.content === "string"
- ? msg.content
- : msg.content.map((part: any) => part.type === "text" ? part.text : "").join("");
-
- // Use the user template to format the enhanced message
- const enhancedContent = await templateCommands.render("chat.user", {
- message: originalContent,
- mentionedContent: processedMentions,
- selectionData: selectionData
- ? {
- text: selectionData.text,
- startOffset: selectionData.startOffset,
- endOffset: selectionData.endOffset,
- sessionId: selectionData.sessionId,
- timestamp: selectionData.timestamp,
+ if (humanData) {
+ processedMentions.push({
+ type: "human",
+ label: mention.label,
+ content: humanContent,
+ });
}
- : undefined,
- });
-
- preparedMessages.push({
- role: "user",
- content: enhancedContent,
- });
- } else {
- // For all other messages, just add them as-is
- preparedMessages.push(msg);
+ }
+ } catch (error) {
+ console.error(`Error fetching content for "${mention.label}":`, error);
+ }
}
}
- return preparedMessages;
+ // Use the user template to format the user message
+ if (currentUserMessage) {
+ const userContent = await templateCommands.render("chat.user", {
+ message: currentUserMessage,
+ mentionedContent: processedMentions,
+ selectionData: selectionData
+ ? {
+ text: selectionData.text,
+ startOffset: selectionData.startOffset,
+ endOffset: selectionData.endOffset,
+ sessionId: selectionData.sessionId,
+ timestamp: selectionData.timestamp,
+ }
+ : undefined, // Convert to plain object for JsonValue compatibility
+ });
+
+ conversationHistory.push({
+ role: "user" as const,
+ content: userContent,
+ });
+ }
+
+ return conversationHistory;
};
diff --git a/apps/desktop/src/components/right-panel/utils/markdown-parser.ts b/apps/desktop/src/components/right-panel/utils/markdown-parser.ts
index b83cf263eb..6ce1908df7 100644
--- a/apps/desktop/src/components/right-panel/utils/markdown-parser.ts
+++ b/apps/desktop/src/components/right-panel/utils/markdown-parser.ts
@@ -1,12 +1,7 @@
-// Simple type for parsed markdown parts
-export interface ParsedPart {
- type: "text" | "markdown";
- content: string;
- isComplete?: boolean;
-}
+import { MessagePart } from "../components/chat/types";
-export const parseMarkdownBlocks = (text: string): ParsedPart[] => {
- const parts: ParsedPart[] = [];
+export const parseMarkdownBlocks = (text: string): MessagePart[] => {
+ const parts: MessagePart[] = [];
let currentIndex = 0;
let inMarkdownBlock = false;
let markdownStart = -1;
diff --git a/apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts b/apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts
index 7196c30c00..282cde25d3 100644
--- a/apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts
+++ b/apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts
@@ -29,13 +29,9 @@ export const createEditEnhancedNoteTool = ({
return { success: false, error: "No session ID available" };
}
- // Skip session store check if sessions object is empty
- // We rely on having a valid sessionId and editor ref instead
- if (sessions && Object.keys(sessions).length > 0) {
- const sessionStore = sessions[sessionId];
- if (!sessionStore) {
- console.warn("Session store not found in sessions object, but continuing with editor ref");
- }
+ const sessionStore = sessions[sessionId];
+ if (!sessionStore) {
+ return { success: false, error: "Session not found" };
}
try {
diff --git a/apps/desktop/src/components/right-panel/views/chat-view.tsx b/apps/desktop/src/components/right-panel/views/chat-view.tsx
index 8ce8cc231f..941e8132fa 100644
--- a/apps/desktop/src/components/right-panel/views/chat-view.tsx
+++ b/apps/desktop/src/components/right-panel/views/chat-view.tsx
@@ -1,12 +1,9 @@
+import { useQuery } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
-import { showProGateModal } from "@/components/pro-gate-modal/service";
import { useHypr, useRightPanel } from "@/contexts";
-import { useLicense } from "@/hooks/use-license";
-import { commands as analyticsCommands } from "@hypr/plugin-analytics";
-import { commands as miscCommands } from "@hypr/plugin-misc";
-import { useSessions } from "@hypr/utils/contexts";
+import { commands as connectorCommands } from "@hypr/plugin-connector";
import {
ChatHistoryView,
ChatInput,
@@ -17,190 +14,73 @@ import {
} from "../components/chat";
import { useActiveEntity } from "../hooks/useActiveEntity";
-import { useChat2 } from "../hooks/useChat2";
-import { useChatQueries2 } from "../hooks/useChatQueries2";
+import { useChatLogic } from "../hooks/useChatLogic";
+import { useChatQueries } from "../hooks/useChatQueries";
+import type { Message } from "../types/chat-types";
import { focusInput, formatDate } from "../utils/chat-utils";
export function ChatView() {
const navigate = useNavigate();
- const { isExpanded, chatInputRef, pendingSelection } = useRightPanel();
+ const { isExpanded, chatInputRef } = useRightPanel();
const { userId } = useHypr();
- const { getLicense } = useLicense();
+ const [messages, setMessages] = useState([]);
const [inputValue, setInputValue] = useState("");
const [showHistory, setShowHistory] = useState(false);
const [searchValue, setSearchValue] = useState("");
- const [currentConversationId, setCurrentConversationId] = useState(null);
+ const [hasChatStarted, setHasChatStarted] = useState(false);
+ const [currentChatGroupId, setCurrentChatGroupId] = useState(null);
const [chatHistory, _setChatHistory] = useState([]);
+ const prevIsGenerating = useRef(false);
+
const { activeEntity, sessionId } = useActiveEntity({
- setMessages: () => {},
+ setMessages,
setInputValue,
setShowHistory,
- setHasChatStarted: () => {},
+ setHasChatStarted,
});
- const sessions = useSessions((s) => s.sessions);
+ const llmConnectionQuery = useQuery({
+ queryKey: ["llm-connection"],
+ queryFn: () => connectorCommands.getLlmConnection(),
+ refetchOnWindowFocus: true,
+ });
- const {
- conversations,
- sessionData,
- createConversation,
- getOrCreateConversationId,
- } = useChatQueries2({
+ const { chatGroupsQuery, sessionData, getChatGroupId } = useChatQueries({
sessionId,
userId,
- currentConversationId,
- setCurrentConversationId,
- setMessages: () => {},
- isGenerating: false,
+ currentChatGroupId,
+ setCurrentChatGroupId,
+ setMessages,
+ setHasChatStarted,
+ prevIsGenerating,
});
const {
- messages,
- stop,
- setMessages,
isGenerating,
- sendMessage,
- status,
- } = useChat2({
+ isStreamingText,
+ handleSubmit,
+ handleQuickAction,
+ handleApplyMarkdown,
+ handleKeyDown,
+ handleStop,
+ } = useChatLogic({
sessionId,
userId,
- conversationId: currentConversationId,
- sessionData: sessionData,
- selectionData: pendingSelection,
- onError: (err: Error) => {
- console.error("Chat error:", err);
- },
+ activeEntity,
+ messages,
+ inputValue,
+ hasChatStarted,
+ setMessages,
+ setInputValue,
+ setHasChatStarted,
+ getChatGroupId,
+ sessionData,
+ chatInputRef,
+ llmConnectionQuery,
});
- useEffect(() => {
- const loadMessages = async () => {
- if (currentConversationId) {
- try {
- const { commands } = await import("@hypr/plugin-db");
- const dbMessages = await commands.listMessagesV2(currentConversationId);
-
- const uiMessages = dbMessages.map(msg => ({
- id: msg.id,
- role: msg.role as "user" | "assistant" | "system",
- parts: JSON.parse(msg.parts),
- metadata: msg.metadata ? JSON.parse(msg.metadata) : {},
- }));
-
- setMessages(uiMessages);
- } catch (error) {
- console.error("Failed to load messages:", error);
- }
- } else {
- setMessages([]);
- }
- };
-
- loadMessages();
- }, [currentConversationId, setMessages]);
-
- const handleSubmit = async (
- mentionedContent?: Array<{ id: string; type: string; label: string }>,
- selectionData?: any,
- htmlContent?: string,
- ) => {
- if (!inputValue.trim()) {
- return;
- }
-
- const userMessageCount = messages.filter((m: any) => m.role === "user").length;
- if (userMessageCount >= 4 && !getLicense.data?.valid) {
- await analyticsCommands.event({
- event: "pro_license_required_chat",
- distinct_id: userId,
- });
- await showProGateModal("chat");
- return;
- }
-
- analyticsCommands.event({
- event: "chat_message_sent",
- distinct_id: userId,
- });
-
- let convId = currentConversationId;
- if (!convId) {
- convId = await getOrCreateConversationId();
- if (!convId) {
- console.error("Failed to create conversation");
- return;
- }
- setCurrentConversationId(convId);
- }
-
- sendMessage(inputValue, {
- mentionedContent,
- selectionData,
- htmlContent,
- conversationId: convId,
- });
-
- setInputValue("");
- };
-
- const handleStop = () => {
- stop();
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- handleSubmit();
- }
- };
-
- const handleQuickAction = async (action: string) => {
- const convId = await createConversation();
- if (!convId) {
- console.error("Failed to create conversation");
- return;
- }
-
- setCurrentConversationId(convId);
-
- sendMessage(action, {
- conversationId: convId,
- });
- };
-
- const handleApplyMarkdown = async (markdownContent: string) => {
- if (!sessionId) {
- console.error("No session ID available");
- return;
- }
-
- const sessionStore = sessions[sessionId];
- if (!sessionStore) {
- console.error("Session not found in store");
- return;
- }
-
- try {
- const html = await miscCommands.opinionatedMdToHtml(markdownContent);
-
- const { showRaw, updateRawNote, updateEnhancedNote } = sessionStore.getState();
-
- if (showRaw) {
- updateRawNote(html);
- } else {
- updateEnhancedNote(html);
- }
- } catch (error) {
- console.error("Failed to apply markdown content:", error);
- }
- };
-
- const isSubmitted = status === "submitted";
- const isStreaming = status === "streaming";
- const isReady = status === "ready";
- const isError = status === "error";
-
const handleInputChange = (e: React.ChangeEvent) => {
setInputValue(e.target.value);
};
@@ -209,29 +89,19 @@ export function ChatView() {
focusInput(chatInputRef);
};
- const handleNewChat = () => {
- if (!messages || messages.length === 0) {
- return;
- }
-
+ const handleNewChat = async () => {
if (!sessionId || !userId) {
return;
}
- if (isGenerating) {
- return;
- }
-
- setCurrentConversationId(null);
- setInputValue("");
+ setCurrentChatGroupId(null);
setMessages([]);
+ setHasChatStarted(false);
+ setInputValue("");
};
const handleSelectChatGroup = async (groupId: string) => {
- if (isGenerating) {
- return;
- }
- setCurrentConversationId(groupId);
+ setCurrentChatGroupId(groupId);
};
const handleViewHistory = () => {
@@ -242,7 +112,7 @@ export function ChatView() {
setSearchValue(e.target.value);
};
- const handleSelectChat = (_chatId: string) => {
+ const handleSelectChat = (chatId: string) => {
setShowHistory(false);
};
@@ -285,7 +155,7 @@ export function ChatView() {
@@ -299,13 +169,11 @@ export function ChatView() {
: (
)}
diff --git a/crates/db-user/src/chat_conversations_migration.sql b/crates/db-user/src/chat_conversations_migration.sql
deleted file mode 100644
index 03214e21dc..0000000000
--- a/crates/db-user/src/chat_conversations_migration.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-CREATE TABLE IF NOT EXISTS chat_conversations (
- id TEXT PRIMARY KEY,
- session_id TEXT NOT NULL,
- user_id TEXT NOT NULL,
- name TEXT,
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL,
- FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
-);
diff --git a/crates/db-user/src/chat_conversations_ops.rs b/crates/db-user/src/chat_conversations_ops.rs
deleted file mode 100644
index 83bdb41eb3..0000000000
--- a/crates/db-user/src/chat_conversations_ops.rs
+++ /dev/null
@@ -1,76 +0,0 @@
-use super::{ChatConversation, UserDatabase};
-
-impl UserDatabase {
- pub async fn create_conversation(
- &self,
- conversation: ChatConversation,
- ) -> Result {
- let conn = self.conn()?;
-
- let mut rows = conn
- .query(
- "INSERT INTO chat_conversations (
- id, session_id, user_id, name, created_at,
-updated_at
- ) VALUES (?, ?, ?, ?, ?, ?)
- RETURNING *",
- vec![
- conversation.id,
- conversation.session_id,
- conversation.user_id,
- conversation.name.unwrap_or_default(),
- conversation.created_at.to_rfc3339(),
- conversation.updated_at.to_rfc3339(),
- ],
- )
- .await?;
-
- let row = rows.next().await?.unwrap();
- let conversation: ChatConversation = libsql::de::from_row(&row)?;
- Ok(conversation)
- }
-
- pub async fn list_conversations(
- &self,
- session_id: impl Into,
- ) -> Result, crate::Error> {
- let conn = self.conn()?;
-
- let mut rows = conn
- .query(
- "SELECT * FROM chat_conversations
- WHERE session_id = ?
- ORDER BY updated_at DESC",
- vec![session_id.into()],
- )
- .await?;
-
- let mut conversations = Vec::new();
- while let Some(row) = rows.next().await? {
- let conversation: ChatConversation = libsql::de::from_row(&row)?;
- conversations.push(conversation);
- }
- Ok(conversations)
- }
-
- pub async fn get_conversation(
- &self,
- id: impl Into,
- ) -> Result