Documentation

Everything you need to build with Speck.js, the AI-native web framework.

Quick Start

Installation

$ npm create speck-app my-app
$ cd my-app
$ npm run dev

Your First Component

Create src/components/Hello.speck:

<Hello>
  <state name={"World"} />

  <div>
    <h1>Hello, {state.name.value}!</h1>
    <input 
      value={state.name.value} 
      onInput={(e) => state.name.value = e.target.value} 
    />
  </div>
</Hello>

Use it anywhere—no imports needed:

<App>
  <Hello />
</App>

Project Structure

my-speck-app/
├── src/
│   ├── components/          # Your .speck components
│   │   ├── App.speck
│   │   ├── Header.speck
│   │   └── Counter.speck
│   ├── .compiled/           # Auto-generated (don't edit)
│   │   ├── App.jsx
│   │   └── _componentRegistry.js
│   └── lib/
│       └── agent-runtime.js # AI agent runtime
├── api/
│   ├── server.js            # API server
│   └── db.js                # Memory database
├── compiler/
│   └── compiler.js          # Speck compiler
├── .env                     # API keys (git-ignored)
├── package.json
└── vite.config.js

CLI Commands

Command Description
npm run dev Start dev server + compiler + AI proxy
npm run dev:no-agent Start without AI proxy
npm run build Production build
npm run compile One-time compile all components
npm run watch Watch for .speck changes

Components

Components are .speck files that combine markup, state, and logic in a clean syntax. The component name is the filename.

<MyComponent>
  <!-- State declarations -->
  <state count={0} />
  
  <!-- Script for functions -->
  <script>
    const increment = () => state.count.value++;
  </script>
  
  <!-- Your markup -->
  <div>
    <p>Count: {state.count.value}</p>
    <button onClick={increment}>+</button>
  </div>
</MyComponent>

State Management

Declare reactive state with <state name={value} />. Access and mutate via state.name.value.

<Counter>
  <state count={0} />
  <state name={"Guest"} />
  
  <p>Hello, {state.name.value}! Count: {state.count.value}</p>
  <button onClick={() => state.count.value++}>+</button>
</Counter>

Script Blocks

Use <script> for component logic, functions, and computed values:

<Calculator>
  <state a={0} />
  <state b={0} />
  
  <script>
    const sum = () => state.a.value + state.b.value;
    const multiply = () => state.a.value * state.b.value;
  </script>
  
  <p>Sum: {sum()}, Product: {multiply()}</p>
</Calculator>

Conditionals (if)

Use <if condition={expr}> for conditional rendering:

<Toggle>
  <state show={false} />
  
  <button onClick={() => state.show.value = !state.show.value}>
    Toggle
  </button>
  
  <if condition={state.show.value}>
    <p>Now you see me!</p>
  </if>
</Toggle>

Loops (loop)

Use <loop of={array} let={item}> to iterate:

<UserList>
  <state users={["Alice", "Bob", "Charlie"]} />
  
  <ul>
    <loop of={state.users.value} let={user}>
      <li>{user}</li>
    </loop>
  </ul>
</UserList>

Switch/Case

Use <switch on={value}> with <case when="x">:

<StatusBadge>
  <props status />
  
  <switch on={props.status}>
    <case when="success">
      <span class="green">✓ Success</span>
    </case>
    <case when="error">
      <span class="red">✗ Error</span>
    </case>
    <default>
      <span>Unknown</span>
    </default>
  </switch>
</StatusBadge>

Routing

Built-in client-side routing with <Router> and <route>:

<App>
  <Router>
    <route path="/">
      <HomePage />
    </route>
    
    <route path="/user/:id" let={params}>
      <UserProfile userId={params.id} />
    </route>
  </Router>
</App>

Async Data

Handle promises declaratively with <async>:

<DataLoader>
  <async promise={fetch('/api/data').then(r => r.json())}>
    <loading>
      <p>Loading...</p>
    </loading>
    
    <then let={data}>
      <p>Got: {data.name}</p>
    </then>
    
    <catch let={error}>
      <p>Error: {error.message}</p>
    </catch>
  </async>
</DataLoader>

Props

Declare accepted props with <props name1 name2 />:

<Button>
  <props label onClick variant />
  
  <button 
    class={props.variant || "primary"}
    onClick={props.onClick}
  >
    {props.label}
  </button>
</Button>

<!-- Usage -->
<Button label="Click me" onClick={handleClick} />

Slots

Use <slot /> for content projection:

<Card>
  <div class="card">
    <div class="card-header">
      <slot name="header" />
    </div>
    <div class="card-body">
      <slot />
    </div>
  </div>
</Card>

<!-- Usage -->
<Card>
  <template slot="header">My Title</template>
  <p>Card content goes here</p>
</Card>

Lifecycle

Use <onMount> for side effects on mount:

<Dashboard>
  <state data={null} />
  
  <onMount>
    {fetch('/api/dashboard').then(r => r.json()).then(d => state.data.value = d)}
  </onMount>
  
  <if condition={state.data.value}>
    <DashboardContent data={state.data.value} />
  </if>
</Dashboard>

AI with Agent Components

Speck.js is AI-native with built-in Agent components. Add AI chat to any component with a single line:

One-Liner Chat UI

<SimpleChat>
  <Agent.Chat purpose="You are a helpful assistant" />
</SimpleChat>

That's it. Full chat interface with input, submit button, streaming responses, error handling, and chat history.

Agent.Chat Props

Prop Type Default Description
id string "default-agent" Unique identifier for memory isolation
purpose string "You are a helpful AI assistant." System prompt
model string "claude-sonnet-4-20250514" Claude model
temperature number 0.7 Response randomness (0-1)
maxTokens number 1000 Max response length
memory boolean false Enable persistent memory
memoryDepth number 100 Number of interactions to remember
placeholder string "Type a message..." Input placeholder
submitLabel string "Send" Button text
showHistory boolean true Show chat history

Agent Memory NEW in v0.4.1

Speck.js agents can remember conversations across browser sessions with a single prop. No configuration, no external services—just persistent memory that works.

Enable Memory

<SmartBot>
  <Agent.Chat
    id="my-assistant"
    purpose="You are a helpful assistant"
    memory={true}
  />
</SmartBot>

How It Works

  • Session ID: Generated on first visit, stored in localStorage, reused forever
  • Interactions: Every message saved to local SQLite database
  • Context Loading: On each message, recent interactions loaded into agent context
  • Persistence: Memory survives page refreshes, browser restarts, and computer reboots

Memory Props

Prop Type Default Description
id string "default-agent" Required for memory. Unique identifier that isolates this agent's memories.
memory boolean false Enable persistent memory across sessions
memoryDepth number 100 Number of past interactions to load into context

Multiple Agents with Separate Memories

<App>
  <!-- Each agent has isolated memory -->
  <Agent.Chat
    id="support-bot"
    purpose="Customer support agent"
    memory={true}
  />
  
  <Agent.Chat
    id="coding-assistant"
    purpose="Help with programming"
    memory={true}
  />
</App>

Customize Memory Depth

<!-- Remember last 50 interactions -->
<Agent.Chat
  id="my-bot"
  memory={true}
  memoryDepth={50}
/>
💡 Tip: The id prop is required when using memory. Without it, all agents would share the same memory pool. Give each agent a unique ID to keep their conversations separate.

Agent.Ask

For single prompt/response tasks like translation, summarization, or extraction:

Basic Usage

<Translator>
  <state input={""} />
  <state output={""} />
  
  <input 
    value={state.input.value} 
    onInput={(e) => state.input.value = e.target.value} 
  />
  
  <Agent.Ask
    purpose="Translate to French. Output only the translation."
    autoSend={false}
    onResponse={(text) => state.output.value = text}
  >
    {({ send, loading }) => (
      <button onClick={() => send(state.input.value)}>
        {loading ? "Translating..." : "Translate"}
      </button>
    )}
  </Agent.Ask>
  
  <p>{state.output.value}</p>
</Translator>

Agent.Ask Props

Prop Type Default Description
id string "default-agent" Unique identifier for memory
purpose string Required System prompt
prompt string - Message to send
autoSend boolean true Send on mount
memory boolean false Enable persistent memory
memoryDepth number 5 Interactions to remember
onResponse function - Called with response
onError function - Called on error

Compound Components

For full customization, use the <Agent> provider with sub-components:

<CustomChat>
  <Agent 
    id="code-reviewer"
    purpose="You are a code reviewer" 
    temperature={0.3}
    memory={true}
  >
    <Agent.History />
    <Agent.Error />
    <Agent.Response />
    <Agent.Input placeholder="Paste code..." />
    <Agent.Submit>Review</Agent.Submit>
    <Agent.Clear>Reset</Agent.Clear>
  </Agent>
</CustomChat>

Available Sub-Components

Component Description
<Agent.Input /> Text input field
<Agent.Submit> Submit button
<Agent.Response /> Response display
<Agent.Loading> Loading indicator (only shows when loading)
<Agent.Error /> Error display (only shows on error)
<Agent.History /> Chat history
<Agent.Clear> Clear history button

Custom Response Rendering

<Agent.Response render={(content) => <Markdown>{content}</Markdown>} />

Agent API Reference

Agent Props

Prop Type Default
id string "default-agent"
purpose string "You are a helpful AI assistant."
model string "claude-sonnet-4-20250514"
temperature number 0.7
maxTokens number 1000
streaming boolean true
memory boolean false
memoryDepth number 100
onResponse (text: string) => void -
onError (error: Error) => void -

Configuration

Add your API key to .env:

VITE_ANTHROPIC_API_KEY=sk-ant-api03-xxxxx

API Reference

Special Tags

Tag Purpose Example
<state> Declare reactive state <state count={0} />
<script> Component logic <script>const fn = () => {}</script>
<if> Conditional rendering <if condition={expr}>...</if>
<loop> List rendering <loop of={arr} let={item}>...</loop>
<switch> Switch statement <switch on={val}><case when="x">...</case></switch>
<Router> Client-side routing <Router><route path="/">...</route></Router>
<route> Route definition <route path="/user/:id" let={params}>...</route>
<async> Async data handling <async promise={...}><then>...</then></async>
<props> Declare accepted props <props name onClick />
<slot> Content insertion point <slot /> or <slot name="header" />
<onMount> Lifecycle hook <onMount>{...code...}</onMount>
<Agent.Chat /> One-liner chat UI <Agent.Chat purpose="..." memory={true} />
<Agent.Ask> Single prompt/response <Agent.Ask purpose="...">...</Agent.Ask>
<Agent> Custom AI chat container <Agent purpose="..." memory={true}>...</Agent>