Towniecredit-additions.ts1 match
2import { formatNumber, formatPrice, formatDate } from "../utils/formatters.ts";
3import { PaginationResult, renderPaginationControls } from "../utils/pagination.ts";
4import { CreditAddition } from "../api/credit-additions.ts";
56interface CreditAdditionsSummary {
79<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
80<title>Drywall AI Assistant</title>
81<link rel="preconnect" href="https://fonts.googleapis.com">
82<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
83<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@400;700&family=Oswald:wght@500&display=swap" rel="stylesheet">
84<style>
85:root {
412<script>
413(function() {
414const API_URL = '${sourceUrl}';
415const $ = (selector) => document.querySelector(selector);
416428}
429430async function handleApiRequest(action, payload, resultsPanel) {
431resultsPanel.style.display = 'none';
432loader.style.display = 'block';
435436try {
437const response = await fetch(API_URL, {
438method: 'POST',
439headers: { 'Content-Type': 'application/json' },
527finish_level: $('#finish-level').value,
528};
529handleApiRequest('estimate', payload, $('#results-estimator'));
530});
531
537}
538const payload = { image: currentImageBase64 };
539handleApiRequest('analyzeImage', payload, $('#results-analyzer'));
540});
541
543e.preventDefault();
544const payload = { description: $('#project-description').value };
545handleApiRequest('getFinishRecommendation', payload, $('#results-finishes'));
546});
547549e.preventDefault();
550const payload = { conditions: $('#site-conditions').value };
551handleApiRequest('safetyCheck', payload, $('#results-safety'));
552});
553559humidity: $('#humidity').value,
560};
561handleApiRequest('calculateMix', payload, $('#results-mixing'));
562});
563
599}
600601// Handle API POST requests
602if (req.method === "POST") {
603const openai = new OpenAI();
IsItTwoWordsApp.tsx1 match
31const fetchWordlist = async () => {
32try {
33const response = await fetch("/api/wordlist");
34const data = await response.json();
35setFullWordlist(data);
104- Test with no MCP servers configured (offline mode)
105- Test with MCP servers that fail to connect
106- Test rapid component attachment/detachment
107- Test page refresh with persistent affordances
108- Test cleanup on navigation away from app
136model: string;
137max_tokens: number;
138anthropic_api_key: string;
139mcp_servers?: Array<{
140type: string;
1718export interface AppConfig {
19anthropicApiKey: string;
20mcpServers: MCPServerConfig[];
21selectedModel: string;
47export default function App() {
48const [config, setConfig] = useState<AppConfig>({
49anthropicApiKey: "",
50mcpServers: [],
51selectedModel: "claude-3-5-sonnet-20241022",
146// Load config from localStorage on mount
147useEffect(() => {
148const savedApiKey = localStorage.getItem("anthropic_api_key");
149const savedMcpServers = localStorage.getItem("mcp_servers");
150const savedMessages = localStorage.getItem("chat_messages");
155let mcpServers = savedMcpServers ? JSON.parse(savedMcpServers) : DEFAULT_MCP_SERVERS;
156setConfig({
157anthropicApiKey: savedApiKey || "",
158mcpServers: mcpServers,
159selectedModel: savedModel || "claude-3-5-sonnet-20241022",
172}
173174// Show settings if no API key is configured
175if (!savedApiKey) {
176setShowSettings(true);
177}
180// Save config to localStorage when it changes
181useEffect(() => {
182if (config.anthropicApiKey) {
183localStorage.setItem("anthropic_api_key", config.anthropicApiKey);
184}
185localStorage.setItem("mcp_servers", config.mcpServers?.length ? JSON.stringify(config.mcpServers) : "");
1# Anthropic Streaming Chat with MCP
23A mobile-optimized single page chat application that uses the Anthropic Messages API with **real-time streaming** and MCP (Model Context Protocol) server support, featuring **centralized client management** and **performance optimizations**.
45Source: https://www.val.town/x/c15r/Chat
40const clientPool = new MCPClientPool(connectedClients, serverConfigs);
4142// Unified API across all components
43await clientPool.testServer(serverName);
44await clientPool.fetchTools();
224225The app stores configuration and chat history in localStorage:
226- `anthropic_api_key`: Your Anthropic API key
227- `selected_model`: The chosen Claude model (defaults to claude-3-5-sonnet-20241022)
228- `mcp_servers`: Array of configured MCP servers
252For detailed testing information, see [TESTING.md](./TESTING.md).
253254### API Endpoints
255256- `GET /` - Main application (serves frontend)
2632641. Open the app at the provided URL
2652. Click "Settings" in the footer to configure your Anthropic API key and select your preferred Claude model
2663. Add/remove/toggle MCP servers as needed
2674. Use the "Test" button next to each MCP server to verify connectivity (shows β for success, β for errors)
308- **Auto-scroll**: Messages automatically scroll to bottom during streaming
309- **Auto-resize**: Input field grows with content
310- **Error Handling**: Clear error messages for API issues with stream recovery
311- **Loading States**: Visual feedback during API calls and streaming
312- **Structured Responses**: MCP tool use and results are displayed in organized, collapsible sections
313- **Clean Interface**: Maximized chat area with no header, footer contains all controls
ChatStreamingChat.tsx8 matches
196/** Retry a user message */
197const retryMessage = async (messageId: string) => {
198if (status !== "idle" || !config.anthropicApiKey) return;
199200const userText = onRetryFromMessage(messageId);
222console.log("[Chat] fire send", { userText, input });
223const messageText = userText || input.trim();
224if (!messageText || status !== "idle" || !config.anthropicApiKey) return;
225226// Only clear input if we're using the current input (not NextSteps execution)
280};
281282const canSend = input?.trim() && status === "idle" && config.anthropicApiKey;
283284/* ββ UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
286<>
287<div className="chat-messages">
288{!config.anthropicApiKey && (
289<div className="message system">
290Please configure your Anthropic API key in settings to start chatting
291</div>
292)}
371}}
372onKeyDown={handleKeyDown}
373placeholder={config.anthropicApiKey
374? streaming
375? "Streamingβ¦"
377? "Waiting for your input aboveβ¦"
378: "Type your message or / for commandsβ¦"
379: "Configure API key in settings first"}
380className="chat-input"
381disabled={!config.anthropicApiKey || thinking || waitingForUser}
382rows={1}
383/>
ChatuseAnthropicStream.tsx7 matches
123/* Anthropic SDK instance β memoised so we don't recreate each render */
124const anthropic = React.useMemo(() => {
125if (!config.anthropicApiKey) return null;
126return new Anthropic({
127dangerouslyAllowBrowser: true,
128apiKey: config.anthropicApiKey,
129baseURL: "https://api.anthropic.com",
130defaultHeaders: {
131"anthropic-version": "2023-06-01",
133},
134});
135}, [config.anthropicApiKey]);
136137/* Abort helper */
167const streamMessage = React.useCallback(
168async (messages: any[]): Promise<{ message: AssistantMsg; stopReason: string }> => {
169if (!anthropic) throw new Error("API key missing");
170171// Use the existing buildBody helper but override messages
192})) as AsyncIterable<MessageStreamEvent>;
193} catch (err: any) {
194console.error("Failed to call Anthropic API", err);
195throw err;
196}
264const send = React.useCallback(
265async (history: Message[], userText: string): Promise<Message[]> => {
266if (!anthropic) throw new Error("API key missing");
267if (status !== "idle") throw new Error("Stream already in progress");
268
digital-scrapbookindex.html1 match
8<meta charset="utf-8" />
9<style>
10@import url("https://fonts.googleapis.com/css2?family=Special+Elite&display=swap");
11</style>
12</head>