From bd49f4280a9117668d3beeb7212e93b3cd12dc18 Mon Sep 17 00:00:00 2001 From: dh Date: Thu, 15 Jan 2026 23:42:32 +0100 Subject: [PATCH 1/6] Update MindCache package dependencies and enhance React components - Updated peer dependencies in `package.json` to require `ai` version 5.0.0 and added `@mindcache/gitstore` as a new optional peer dependency. - Modified TypeScript configuration to use `react-jsx` for JSX transformation. - Expanded exports in `index.ts` to include new local-first React components and hooks, enhancing the API surface for developers. - Introduced `MindCacheChat` component for a ready-to-use chat interface, including customizable themes and message handling. - Added context management with `MindCacheContext` to facilitate state sharing across components. - Implemented `useClientChat` hook for managing chat interactions and real-time message streaming. - Created `useLocalFirstSync` hook for automatic syncing with GitStore, providing a robust solution for local-first data management. --- packages/mindcache/package.json | 8 +- packages/mindcache/src/index.ts | 30 + .../mindcache/src/react/MindCacheChat.tsx | 553 ++++++++++++++++++ .../mindcache/src/react/MindCacheContext.tsx | 265 +++++++++ packages/mindcache/src/react/index.ts | 37 ++ packages/mindcache/src/react/useClientChat.ts | 351 +++++++++++ .../mindcache/src/react/useLocalFirstSync.ts | 341 +++++++++++ packages/mindcache/tsconfig.json | 1 + 8 files changed, 1584 insertions(+), 2 deletions(-) create mode 100644 packages/mindcache/src/react/MindCacheChat.tsx create mode 100644 packages/mindcache/src/react/MindCacheContext.tsx create mode 100644 packages/mindcache/src/react/useClientChat.ts create mode 100644 packages/mindcache/src/react/useLocalFirstSync.ts diff --git a/packages/mindcache/package.json b/packages/mindcache/package.json index f0279fb..151e64c 100644 --- a/packages/mindcache/package.json +++ b/packages/mindcache/package.json @@ -80,12 +80,16 @@ "zod": "^3.23.0" }, "peerDependencies": { - "ai": ">=3.0.0", - "react": "^19.0.0" + "ai": ">=5.0.0", + "@mindcache/gitstore": ">=0.1.0", + "react": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "ai": { "optional": true + }, + "@mindcache/gitstore": { + "optional": true } }, "engines": { diff --git a/packages/mindcache/src/index.ts b/packages/mindcache/src/index.ts index 5d29961..407725c 100644 --- a/packages/mindcache/src/index.ts +++ b/packages/mindcache/src/index.ts @@ -40,3 +40,33 @@ export type { IndexedDBConfig } from './local'; // React exports export { useMindCache } from './react'; export type { UseMindCacheResult } from './react'; + +// Local-first React components and hooks +export { + MindCacheProvider, + useMindCacheContext, + MindCacheChat, + useClientChat, + useLocalFirstSync +} from './react'; + +export type { + // Provider types + MindCacheProviderConfig, + MindCacheContextValue, + LocalFirstSyncConfig, + AIConfig, + // Chat types + MindCacheChatProps, + ChatTheme, + UseClientChatOptions, + UseClientChatReturn, + ChatMessage, + ChatStatus, + // Sync types + UseLocalFirstSyncOptions, + UseLocalFirstSyncReturn, + GitStoreSyncConfig, + ServerSyncConfig, + SyncStatus +} from './react'; diff --git a/packages/mindcache/src/react/MindCacheChat.tsx b/packages/mindcache/src/react/MindCacheChat.tsx new file mode 100644 index 0000000..6433424 --- /dev/null +++ b/packages/mindcache/src/react/MindCacheChat.tsx @@ -0,0 +1,553 @@ +'use client'; + +import React, { useState, useRef, useEffect, type FormEvent, type KeyboardEvent } from 'react'; +import { useClientChat, type ChatMessage, type UseClientChatOptions } from './useClientChat'; +import { useMindCacheContext } from './MindCacheContext'; + +/** + * Chat theme configuration + */ +export interface ChatTheme { + /** Container background */ + background?: string; + /** User message background */ + userBubble?: string; + /** Assistant message background */ + assistantBubble?: string; + /** Text color */ + textColor?: string; + /** Secondary text color */ + secondaryTextColor?: string; + /** Border color */ + borderColor?: string; + /** Primary/accent color */ + primaryColor?: string; + /** Font family */ + fontFamily?: string; +} + +/** + * Default dark theme + */ +const defaultTheme: ChatTheme = { + background: '#000', + userBubble: '#1a1a2e', + assistantBubble: '#0d0d0d', + textColor: '#22c55e', + secondaryTextColor: '#6b7280', + borderColor: '#333', + primaryColor: '#22c55e', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' +}; + +/** + * MindCacheChat props + */ +export interface MindCacheChatProps extends Omit { + /** Custom theme */ + theme?: ChatTheme; + /** Placeholder text for input */ + placeholder?: string; + /** Welcome message (shown when no messages) */ + welcomeMessage?: string; + /** Show API key input if not configured */ + showApiKeyInput?: boolean; + /** Custom class name for container */ + className?: string; + /** Custom styles for container */ + style?: React.CSSProperties; + /** Render custom message component */ + renderMessage?: (message: ChatMessage) => React.ReactNode; + /** Header component */ + header?: React.ReactNode; + /** Footer component (below input) */ + footer?: React.ReactNode; +} + +/** + * Default message renderer + */ +function DefaultMessage({ + message, + theme +}: { + message: ChatMessage; + theme: ChatTheme; +}) { + const isUser = message.role === 'user'; + + return ( +
+
+
+ {isUser ? 'You' : 'Assistant'} +
+
+ {message.content} +
+
+
+ ); +} + +/** + * API Key input component + */ +function ApiKeyInput({ + theme, + onSubmit +}: { + theme: ChatTheme; + onSubmit: (key: string) => void; +}) { + const [key, setKey] = useState(''); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + if (key.trim()) { + onSubmit(key.trim()); + } + }; + + return ( +
+
+ Enter your API key to start chatting +
+
+ setKey(e.target.value)} + placeholder="sk-..." + style={{ + width: '100%', + padding: '12px', + backgroundColor: theme.assistantBubble, + border: `1px solid ${theme.borderColor}`, + borderRadius: '8px', + color: theme.textColor, + fontFamily: theme.fontFamily, + fontSize: '14px', + marginBottom: '12px' + }} + /> + +
+
+ Your key is stored locally and never sent to our servers. +
+
+ ); +} + +/** + * MindCacheChat - Ready-to-use chat component for local-first AI + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function MindCacheChat({ + theme: customTheme, + placeholder = 'Type a message...', + welcomeMessage = 'Hello! I\'m ready to help you.', + showApiKeyInput = true, + className, + style, + renderMessage, + header, + footer, + initialMessages, + ...chatOptions +}: MindCacheChatProps) { + const context = useMindCacheContext(); + const theme = { ...defaultTheme, ...customTheme }; + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + const [inputValue, setInputValue] = useState(''); + + // Initialize with welcome message if no initial messages + const defaultInitialMessages: ChatMessage[] = welcomeMessage ? [ + { + id: 'welcome', + role: 'assistant', + content: welcomeMessage, + createdAt: new Date() + } + ] : []; + + const { + messages, + sendMessage, + isLoading, + error, + streamingContent, + stop + } = useClientChat({ + ...chatOptions, + initialMessages: initialMessages || defaultInitialMessages, + mindcache: context.mindcache || undefined + }); + + // Auto-scroll to bottom + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + // Handle submit + const handleSubmit = async (e?: FormEvent) => { + e?.preventDefault(); + if (!inputValue.trim() || isLoading) { + return; + } + + const message = inputValue.trim(); + setInputValue(''); + await sendMessage(message); + }; + + // Handle keyboard + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + }; + + // Show API key input if needed + if (showApiKeyInput && !context.hasApiKey && context.aiConfig.keyStorage !== 'memory') { + return ( +
+ {header} + context.setApiKey(key)} + /> + {footer} +
+ ); + } + + // Loading state + if (!context.isLoaded) { + return ( +
+ Loading... +
+ ); + } + + return ( +
+ {header} + + {/* Messages area */} +
+ {messages.map((message) => ( + renderMessage ? ( + + {renderMessage(message)} + + ) : ( + + ) + ))} + + {/* Show streaming content in real-time */} + {streamingContent && ( +
+
+
+ Assistant +
+
+ {streamingContent} + +
+
+
+ )} + + {/* Show loading indicator only when not streaming yet */} + {isLoading && !streamingContent && ( +
+
+ Thinking... +
+
+ )} + + {error && ( +
+ Error: {error.message} +
+ )} + +
+
+ + {/* Input area */} +
+
+