commit 98ff1e09e02bc7420e39d714290898c92c6ebe6b Author: shakespeare.diy Date: Mon Feb 16 16:54:01 2026 -0600 New project created with Shakespeare Co-authored-by: shakespeare.diy diff --git a/.claude/skills/ai-chat/SKILL.md b/.claude/skills/ai-chat/SKILL.md new file mode 100644 index 00000000..4a8f596a --- /dev/null +++ b/.claude/skills/ai-chat/SKILL.md @@ -0,0 +1,337 @@ +--- +name: ai-chat +description: Build AI-powered chat interfaces, implement streaming responses, or integrate with Shakespeare AI. +--- + +# AI Integration with Shakespeare API + +Use the `useShakespeare` hook for AI chat completions with Nostr authentication. The API dynamically provides available models, so you should query them at runtime rather than hardcoding model names. + +```tsx +import { useShakespeare, type ChatMessage, type Model } from '@/hooks/useShakespeare'; + +const { + sendChatMessage, + sendStreamingMessage, + getAvailableModels, + isLoading, + error, + isAuthenticated +} = useShakespeare(); +``` + +#### Model Selector Component + +```tsx +function ModelSelector({ onModelSelect }: { onModelSelect: (modelId: string) => void }) { + const { getAvailableModels, isLoading } = useShakespeare(); + const [models, setModels] = useState([]); + const [selectedModel, setSelectedModel] = useState(''); + + useEffect(() => { + const fetchModels = async () => { + try { + const response = await getAvailableModels(); + // Sort models by total cost (cheapest first) + const sortedModels = response.data.sort((a, b) => { + const costA = parseFloat(a.pricing.prompt) + parseFloat(a.pricing.completion); + const costB = parseFloat(b.pricing.prompt) + parseFloat(b.pricing.completion); + return costA - costB; + }); + setModels(sortedModels); + + // Select the cheapest model by default + if (sortedModels.length > 0) { + const cheapestModel = sortedModels[0]; + setSelectedModel(cheapestModel.id); + onModelSelect(cheapestModel.id); + } + } catch (err) { + console.error('Failed to fetch models:', err); + } + }; + + fetchModels(); + }, [getAvailableModels, onModelSelect]); + + const handleModelChange = (modelId: string) => { + setSelectedModel(modelId); + onModelSelect(modelId); + }; + + return ( +
+ + +
+ ); +} +``` + +#### Basic Chat Example + +```tsx +function AIChat() { + const { sendChatMessage, isLoading, error, isAuthenticated } = useShakespeare(); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [selectedModel, setSelectedModel] = useState(''); + + const handleSend = async () => { + if (!input.trim() || !selectedModel) return; + + const newMessages = [...messages, { role: 'user', content: input }]; + setMessages(newMessages); + setInput(''); + + try { + const response = await sendChatMessage(newMessages, selectedModel); + setMessages(prev => [...prev, { + role: 'assistant', + content: response.choices[0].message.content as string + }]); + } catch (err) { + console.error('Chat error:', err); + } + }; + + if (!isAuthenticated) return
Please log in to use AI
; + + return ( +
+ {error &&
{error}
} + + {/* Model Selection */} +
+ +
+ +
+ {messages.map((msg, i) => ( +
+ {msg.role}: {msg.content} +
+ ))} +
+ +
+ setInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSend()} + className="flex-1 p-2 border rounded" + disabled={isLoading || !selectedModel} + placeholder={!selectedModel ? "Select a model first..." : "Type your message..."} + /> + +
+
+ ); +} +``` + +#### Streaming Chat Example + +```tsx +function StreamingChat() { + const { sendStreamingMessage } = useShakespeare(); + const [messages, setMessages] = useState([]); + const [currentResponse, setCurrentResponse] = useState(''); + const [selectedModel, setSelectedModel] = useState(''); + + const handleStreaming = async (content: string) => { + if (!selectedModel) return; + + setCurrentResponse(''); + const newMessages = [...messages, { role: 'user', content }]; + setMessages(newMessages); + + try { + await sendStreamingMessage(newMessages, selectedModel, (chunk) => { + setCurrentResponse(prev => prev + chunk); + }); + + // Add the complete response to messages + if (currentResponse.trim()) { + setMessages(prev => [...prev, { + role: 'assistant', + content: currentResponse + }]); + } + } catch (err) { + console.error('Streaming error:', err); + } finally { + setCurrentResponse(''); + } + }; + + return ( +
+ {/* Model selection UI */} +
+ +
+ + {/* Chat interface */} + {/* ... rest of your chat UI */} +
+ ); +} +``` + +#### Model Information + +Models are dynamically fetched from the Shakespeare API and include: + +- **Model ID**: Unique identifier for the model +- **Name**: Human-readable model name +- **Description**: Model capabilities and use cases +- **Context Window**: Maximum token limit for conversations +- **Pricing**: Cost per token for prompt and completion +- **Free Models**: Models with `pricing.prompt === "0"` and `pricing.completion === "0"` + +#### Key Points + +- **Dynamic Model Discovery**: Always fetch available models using `getAvailableModels()` +- **Authentication Required**: User must be logged in with Nostr account +- **Free vs Premium**: Check pricing to determine if model requires credits +- **Error Handling**: Handle `isLoading` and `error` states appropriately +- **Model Selection**: Provide UI for users to choose between available models + +## Implementation Patterns and Best Practices + +### Dialog Component Patterns + +When using Dialog components, always ensure accessibility compliance by including required elements: + +```tsx +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; + +// ✅ Correct - Always include DialogHeader with DialogTitle + + + + Dialog Title + + Optional description for screen readers + + + {/* Dialog content */} + + +``` + +**Important**: Even if you want to hide the title visually, use the `VisuallyHidden` component to maintain accessibility: + +```tsx +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; + + + + Hidden Title for Screen Readers + + +``` + +### Streaming Response Handling + +When implementing streaming chat interfaces, always accumulate streamed content in a local variable before clearing the streaming state to prevent content loss: + +```tsx +const handleStreamingResponse = async () => { + let streamedContent = ''; // ✅ Use local variable to accumulate content + + try { + await sendStreamingMessage(messages, model, (chunk) => { + streamedContent += chunk; // ✅ Accumulate in local variable + setCurrentStreamingMessage(streamedContent); // Update UI + }); + + // ✅ Save accumulated content to persistent state + if (streamedContent.trim()) { + const assistantMessage: MessageDisplay = { + id: Date.now().toString(), + role: 'assistant', + content: streamedContent, // ✅ Use accumulated content + timestamp: new Date() + }; + setMessages(prev => [...prev, assistantMessage]); + } + } finally { + setCurrentStreamingMessage(''); // ✅ Clear streaming state after saving + } +}; +``` + +### Error Boundary Patterns + +Always wrap AI components with error boundaries and provide user-friendly error messages for common failure scenarios: + +```tsx +import { ErrorBoundary } from '@/components/ErrorBoundary'; +import { Alert, AlertDescription } from '@/components/ui/alert'; + +function AIChatWithErrorBoundary() { + return ( + + + + + Something went wrong with the AI chat. Please refresh the page and try again. + + + + } + > + + + ); +} + +// In your AI component, handle specific error types gracefully: +function useAIWithErrorHandling() { + const { sendChatMessage, error, clearError } = useShakespeare(); + + const sendMessage = async (messages: ChatMessage[], modelId: string) => { + try { + await sendChatMessage(messages, modelId); + } catch (err) { + // Handle specific error types with user-friendly messages + if (err.message.includes('401')) { + throw new Error('Authentication failed. Please log in again.'); + } else if (err.message.includes('402')) { + throw new Error('Insufficient credits. Please add credits to use premium features.'); + } else if (err.message.includes('network')) { + throw new Error('Network error. Please check your internet connection.'); + } + throw err; // Re-throw for error boundary + } + }; + + return { sendMessage, error, clearError }; +} +``` diff --git a/.claude/skills/nostr-comments/SKILL.md b/.claude/skills/nostr-comments/SKILL.md new file mode 100644 index 00000000..1ca90648 --- /dev/null +++ b/.claude/skills/nostr-comments/SKILL.md @@ -0,0 +1,59 @@ +--- +name: nostr-comments +description: Implement Nostr comment systems, add discussion features to posts/articles, or build community interaction features. +--- + +# Adding Nostr Comments Sections + +The project includes a complete commenting system using NIP-22 (kind 1111) comments that can be added to any Nostr event or URL. The `CommentsSection` component provides a full-featured commenting interface with threaded replies, user authentication, and real-time updates. + +## Basic Usage + +```tsx +import { CommentsSection } from "@/components/comments/CommentsSection"; + +function ArticlePage({ article }: { article: NostrEvent }) { + return ( +
+ {/* Your article content */} +
{/* article content */}
+ + {/* Comments section */} + +
+ ); +} +``` + +## Props and Customization + +The `CommentsSection` component accepts the following props: + +- **`root`** (required): The root event or URL to comment on. Can be a `NostrEvent` or `URL` object. +- **`title`**: Custom title for the comments section (default: "Comments") +- **`emptyStateMessage`**: Message shown when no comments exist (default: "No comments yet") +- **`emptyStateSubtitle`**: Subtitle for empty state (default: "Be the first to share your thoughts!") +- **`className`**: Additional CSS classes for styling +- **`limit`**: Maximum number of comments to load (default: 500) + +```tsx + +``` + +## Commenting on URLs + +The comments system also supports commenting on external URLs, making it useful for web pages, articles, or any online content: + +```tsx + +``` diff --git a/.claude/skills/nostr-direct-messages/SKILL.md b/.claude/skills/nostr-direct-messages/SKILL.md new file mode 100644 index 00000000..26f3ef56 --- /dev/null +++ b/.claude/skills/nostr-direct-messages/SKILL.md @@ -0,0 +1,478 @@ +--- +name: nostr-direct-messages +description: Implement Nostr direct messaging features, build chat interfaces, or work with encrypted peer-to-peer communication (NIP-04 and NIP-17). +--- + +# Direct Messaging on Nostr + +This project includes a complete direct messaging system supporting both NIP-04 (legacy) and NIP-17 (modern, more private) encrypted messages with real-time subscriptions, optimistic updates, and a persistent cache-first local storage. + +**The DM system is not enabled by default** - follow the setup instructions below to add messaging functionality to your application. + +## Setup Instructions + +### 1. Add DMProvider to Your App + +First, add the `DMProvider` to your app's provider tree in `src/App.tsx`: + +```tsx +// Add these imports at the top of src/App.tsx +import { DMProvider, type DMConfig } from '@/components/DMProvider'; +import { PROTOCOL_MODE } from '@/lib/dmConstants'; + +// Add this configuration before your App component +const dmConfig: DMConfig = { + // Enable or disable DMs entirely + enabled: true, // Set to true to enable messaging functionality + + // Choose one protocol mode: + // PROTOCOL_MODE.NIP04_ONLY - Force NIP-04 (legacy) only + // PROTOCOL_MODE.NIP17_ONLY - Force NIP-17 (private) only + // PROTOCOL_MODE.NIP04_OR_NIP17 - Allow users to choose between NIP-04 and NIP-17 (defaults to NIP-17) + protocolMode: PROTOCOL_MODE.NIP17_ONLY, // Recommended for new apps +}; + +// Then wrap your app components with DMProvider: +export function App() { + return ( + + + + + + + + + + + + + + + + + + + + ); +} +``` + +### 2. Configure DM Settings + +The `DMConfig` object supports the following options: + +- `enabled` (boolean, default: `false`) - Enable/disable entire DM system. When false, no messages are loaded, stored, or processed. +- `protocolMode` (ProtocolMode, default: `PROTOCOL_MODE.NIP17_ONLY`) - Which protocols to support: + - `PROTOCOL_MODE.NIP04_ONLY` - Legacy encryption only + - `PROTOCOL_MODE.NIP17_ONLY` - Modern private messages (recommended) + - `PROTOCOL_MODE.NIP04_OR_NIP17` - Support both protocols (for backwards compatibility) + +**Note**: The DM system uses domain-based IndexedDB naming (`nostr-dm-store-${hostname}`) to prevent conflicts between multiple apps on the same domain. + +## Quick Start + +### 1. Send Messages + +```tsx +import { useDMContext } from '@/hooks/useDMContext'; +import { MESSAGE_PROTOCOL } from '@/lib/dmConstants'; + +function ComposeMessage({ recipientPubkey }: { recipientPubkey: string }) { + const { sendMessage } = useDMContext(); + const [content, setContent] = useState(''); + + const handleSend = async () => { + await sendMessage({ + recipientPubkey, + content, + protocol: MESSAGE_PROTOCOL.NIP17, // Uses NIP-44 encryption + gift wrapping + }); + setContent(''); + }; + + return ( +
{ e.preventDefault(); handleSend(); }}> +