import { useState, useRef, useEffect } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' const URL_REGEX = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=%]*)?$/i const initialForm = { userName: '', userEmail: '', stationName: '', stationLocation: '', stationWebsite: '', } export default function App() { const [form, setForm] = useState(initialForm) const [errors, setErrors] = useState({}) const [isSubmitting, setIsSubmitting] = useState(false) const [isThinking, setIsThinking] = useState(false) const [isStreaming, setIsStreaming] = useState(false) const [result, setResult] = useState('') const [streamError, setStreamError] = useState(null) const resultRef = useRef(null) useEffect(() => { if (resultRef.current) { resultRef.current.scrollTop = resultRef.current.scrollHeight } }, [result]) const handleChange = (e) => { const { name, value } = e.target setForm((prev) => ({ ...prev, [name]: value })) } const validate = () => { const next = {} if (!form.userName.trim()) next.userName = 'Required' if (!form.userEmail.trim() || !/^\S+@\S+\.\S+$/.test(form.userEmail)) next.userEmail = 'Valid email required' if (!form.stationName.trim()) next.stationName = 'Required' if (!form.stationLocation.trim()) next.stationLocation = 'Required' if (!form.stationWebsite.trim() || !URL_REGEX.test(form.stationWebsite)) next.stationWebsite = 'Valid URL required' setErrors(next) return Object.keys(next).length === 0 } const readStream = async (response) => { const reader = response.body.getReader() const decoder = new TextDecoder('utf-8') let firstChunk = true while (true) { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value, { stream: true }) if (firstChunk) { // Once data starts flowing, flip 'thinking' off and 'streaming' on. setIsThinking(false) setIsStreaming(true) firstChunk = false } setResult((prev) => prev + chunk) } // Flush any remaining buffered bytes const tail = decoder.decode() if (tail) setResult((prev) => prev + tail) setIsStreaming(false) } const handleSubmit = async (e) => { e.preventDefault() if (!validate()) return setResult('') setStreamError(null) setIsSubmitting(true) setIsThinking(true) setIsStreaming(false) try { const response = await fetch('/.netlify/functions/agent-proxy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form), }) if (!response.ok || !response.body) { throw new Error( `Request failed: ${response.status} ${response.statusText}`, ) } await readStream(response) } catch (err) { console.error(err) setStreamError(err.message || 'Something went wrong while streaming.') } finally { setIsSubmitting(false) setIsThinking(false) setIsStreaming(false) } } return (

PMC Funder Discovery Tool

Tell us about your public media station and we'll have an Anthropic-powered agent draft a tailored funding outlook.

{isSubmitting && !isStreaming && ( {isThinking ? ( <> Claude is thinking… ) : ( <> Connecting to Agent… )} )} {streamError && (
{streamError}
)} {(result || isStreaming) && (
{result} {isStreaming && }
)}
) } function Field({ label, name, type = 'text', value, onChange, error, placeholder }) { return ( ) } function StatusBanner({ children }) { return (
{children}
) } function Spinner() { return ( ) }