بنقرة واحدة
ai-chat
Build AI-powered chat interfaces, implement streaming responses, or integrate with Shakespeare AI.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Build AI-powered chat interfaces, implement streaming responses, or integrate with Shakespeare AI.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Ditto's release and publishing pipeline — cutting a version tag, Zapstore APK publishing with NIP-46 bunker auth, nsite web deploys via nsyte, and Google Play AAB uploads via fastlane supply. Includes GitLab CI variable setup and credential rotation.
Operate the self-hosted GitLab Runner on the Mac that builds Ditto's iOS IPA. Covers SSH access, restarting the runner, viewing logs, updating Xcode, debugging fastlane locally, and rotating match certificates.
Publish a new app release with versioning, changelog, native build files, and git tagging. Triggered by "publish a new release" or similar requests.
Decide whether to reuse an existing NIP or mint a new kind, design tag structures that relays can index, choose what goes in content vs. tags, and document new kinds or extensions in NIP.md. Load when authoring a new schema — not when wiring up rendering for a kind that already exists (use nostr-kind-rendering for that).
Add UI rendering for an event kind Ditto doesn't yet display — feed cards, detail pages, embedded previews, notifications, routes, feed-toggle registration, and the several kind-label maps (KIND_LABELS, KIND_HEADER_MAP, NOTIFICATION_KIND_NOUNS, CommentContext) that must stay in sync. Load when asked to "support / display / render" a NIP or kind number, when a kind renders blank or as "Kind 12345", or when quote embeds of a kind show "This event kind is not supported".
Browser-API gotchas inside Capacitor's WKWebView (iOS) and Android WebView — which common web APIs silently fail, the downloadTextFile/openUrl helpers that bridge web and native, platform detection, and the installed Capacitor plugins. Load when writing code that interacts with file downloads, external URLs, or platform-specific behavior.
| name | ai-chat |
| description | Build AI-powered chat interfaces, implement streaming responses, or integrate with Shakespeare AI. |
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.
import { useShakespeare, type ChatMessage, type Model } from '@/hooks/useShakespeare';
const {
sendChatMessage,
sendStreamingMessage,
getAvailableModels,
isLoading,
error,
isAuthenticated
} = useShakespeare();
function ModelSelector({ onModelSelect }: { onModelSelect: (modelId: string) => void }) {
const { getAvailableModels, isLoading } = useShakespeare();
const [models, setModels] = useState<Model[]>([]);
const [selectedModel, setSelectedModel] = useState<string>('');
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 (
<div>
<label htmlFor="model-select">Choose Model:</label>
<select
id="model-select"
value={selectedModel}
onChange={(e) => handleModelChange(e.target.value)}
disabled={isLoading}
>
<option value="">Select a model...</option>
{models.map((model, index) => {
const totalCost = parseFloat(model.pricing.prompt) + parseFloat(model.pricing.completion);
const isCheapest = index === 0;
return (
<option key={model.id} value={model.id}>
{model.name} - {isCheapest ? "Cheapest" : `$${totalCost.toFixed(6)}/token`}
</option>
);
})}
</select>
</div>
);
}
function AIChat() {
const { sendChatMessage, isLoading, error, isAuthenticated } = useShakespeare();
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState('');
const [selectedModel, setSelectedModel] = useState<string>('');
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 <div>Please log in to use AI</div>;
return (
<div className="max-w-2xl mx-auto p-4">
{error && <div className="text-red-500 mb-4">{error}</div>}
{/* Model Selection */}
<div className="mb-4">
<ModelSelector onModelSelect={setSelectedModel} />
</div>
<div className="space-y-2 mb-4">
{messages.map((msg, i) => (
<div key={i} className={`p-2 rounded ${msg.role === 'user' ? 'bg-blue-100' : 'bg-gray-100'}`}>
<strong>{msg.role}:</strong> {msg.content}
</div>
))}
</div>
<div className="flex gap-2">
<input
value={input}
onChange={(e) => 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..."}
/>
<button
onClick={handleSend}
disabled={isLoading || !selectedModel}
className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
Send
</button>
</div>
</div>
);
}
function StreamingChat() {
const { sendStreamingMessage } = useShakespeare();
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [currentResponse, setCurrentResponse] = useState('');
const [selectedModel, setSelectedModel] = useState<string>('');
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 (
<div>
{/* Model selection UI */}
<div className="mb-4">
<ModelSelector onModelSelect={setSelectedModel} />
</div>
{/* Chat interface */}
{/* ... rest of your chat UI */}
</div>
);
}
Models are dynamically fetched from the Shakespeare API and include:
pricing.prompt === "0" and pricing.completion === "0"getAvailableModels()isLoading and error states appropriatelyWhen using Dialog components, always ensure accessibility compliance by including required elements:
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
// ✅ Correct - Always include DialogHeader with DialogTitle
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>
Optional description for screen readers
</DialogDescription>
</DialogHeader>
{/* Dialog content */}
</DialogContent>
</Dialog>
Important: Even if you want to hide the title visually, use the VisuallyHidden component to maintain accessibility:
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
<DialogHeader>
<VisuallyHidden>
<DialogTitle>Hidden Title for Screen Readers</DialogTitle>
</VisuallyHidden>
</DialogHeader>
When implementing streaming chat interfaces, always accumulate streamed content in a local variable before clearing the streaming state to prevent content loss:
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
}
};
Always wrap AI components with error boundaries and provide user-friendly error messages for common failure scenarios:
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { Alert, AlertDescription } from '@/components/ui/alert';
function AIChatWithErrorBoundary() {
return (
<ErrorBoundary
fallback={
<div className="p-4">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Something went wrong with the AI chat. Please refresh the page and try again.
</AlertDescription>
</Alert>
</div>
}
>
<AIChat />
</ErrorBoundary>
);
}
// 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 };
}