encrier

Save state format reference · about this project

An encrier save state is a self-contained HTML file that preserves a conversation. The file works offline, requires no external dependencies, and can be uploaded back into a future conversation to restore full context. This page documents the format.

Overview

A conformant save state is a single .html file containing all CSS in an inline style block. It uses system fonts only — no Google Fonts or external stylesheets. It does not use localStorage, sessionStorage, or position:fixed. It is built with vanilla HTML, CSS, and JavaScript only, no frameworks.

Metadata header

A JSON metadata block appears at the very top of the file, before the DOCTYPE, inside an HTML comment:

<!--ENCRIER-SAVE-STATE
{
  "version": "2.0",
  "generated": "YYYY-MM-DD",
  "message_count": 24,
  "topics": ["topic 1", "topic 2"],
  "key_decisions": ["decision 1", "decision 2"],
  "open_questions": ["question 1"],
  "files_generated": ["report.docx"],
  "widgets_embedded": 1
}
-->

Content rules

Condensation protocol

Conversations under ~25 messages are reproduced in full. For longer conversations:

  1. Never condensed: User messages (always verbatim), final decisions/deliverables, final code versions.
  2. Moderately condensed: Explanatory responses — tightened prose, all key points preserved.
  3. Aggressively condensed: Intermediate iterations, tangential discussions, acknowledgments.

Condensed messages include an italic note: "This response has been condensed. The full version covered [brief description]."

Widget embedding

The format does not use <iframe srcdoc='...'> with raw HTML in the attribute. Two approaches are used instead:

Approach A — Inline DOM (widgets with no JavaScript): The widget markup is placed directly in the page with scoped CSS classes prefixed ew-[widgetname]-.

Approach B — Template + loader (widgets with JavaScript): The widget HTML is stored inside a <script type="text/encrier-widget"> tag. A loader script injects it into a sandboxed iframe at runtime:

<script type="text/encrier-widget" id="widget-pricing">
  <!-- full widget HTML/CSS/JS here -->
</script>
<iframe id="frame-pricing" class="encrier-widget-frame"
  sandbox="allow-scripts" loading="lazy"></iframe>

<script>
  document.querySelectorAll('script[type="text/encrier-widget"]')
    .forEach(function(tpl) {
      var id = tpl.id.replace('widget-', 'frame-');
      var frame = document.getElementById(id);
      if (frame) { frame.srcdoc = tpl.textContent; }
    });
</script>

The decision rule: no script tags or event handlers in the widget → Approach A. Any JavaScript → Approach B. If too complex → a styled placeholder.

Round-trip markup

Every message container carries data-encrier-* attributes for machine parsing:

<div class="msg msg-user" data-encrier-role="user" data-encrier-index="1">...</div>
<div class="msg msg-assistant" data-encrier-role="assistant" data-encrier-index="2">...</div>

Additional markers: data-encrier-type="widget", data-encrier-type="file-badge" with data-encrier-filename="...", data-encrier-type="image-placeholder", data-encrier-condensed="true".

CSS template

All conformant save states use this exact CSS. It is copied verbatim into the file's style block:

/* Encrier Save State CSS Template v2 */
:root {
  --bg-page: #f6f3ed; --bg-card-user: #eae7e0; --bg-card-asst: #ffffff;
  --bg-code: #1e1e2a; --text: #1a1612; --text-secondary: #635a52;
  --text-muted: #928980; --text-code: #c8c4bc; --border: #e2dfdb;
  --accent-user: #b8632e; --accent-asst: #3d3680;
  --accent-file: #1a6b5a; --accent-file-bg: #e4f4ee;
  --font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-display: Georgia, 'Times New Roman', serif;
  --font-mono: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
}
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
body { font-family:var(--font-body); background:var(--bg-page);
  color:var(--text); line-height:1.7; font-size:15px; }
.encrier-header { background:linear-gradient(135deg,#2a1f3e,#1a1228);
  padding:3rem 2rem; text-align:center; }
.encrier-header h1 { font-family:var(--font-display); color:#fff;
  font-size:1.8rem; font-weight:normal; }
.encrier-header p { color:rgba(255,255,255,0.45); font-size:0.8rem;
  margin-top:0.4rem; letter-spacing:0.04em; }
.encrier-chat { max-width:820px; margin:0 auto; padding:2rem 1.5rem 4rem; }
.msg { margin-bottom:2.5rem; }
.msg-role { font-size:0.7rem; font-weight:600; text-transform:uppercase;
  letter-spacing:0.1em; margin-bottom:0.4rem; display:flex;
  align-items:center; gap:0.5rem; }
.msg-role .dot { width:6px; height:6px; border-radius:50%; display:inline-block; }
.msg-user .msg-role { color:var(--accent-user); }
.msg-user .msg-role .dot { background:var(--accent-user); }
.msg-assistant .msg-role { color:var(--accent-asst); }
.msg-assistant .msg-role .dot { background:var(--accent-asst); }
.msg-body { padding:1.25rem 1.5rem; border-radius:12px; border:1px solid var(--border); }
.msg-user .msg-body { background:var(--bg-card-user); }
.msg-assistant .msg-body { background:var(--bg-card-asst);
  box-shadow:0 1px 3px rgba(0,0,0,0.04); }
.msg-body p { margin-bottom:0.8rem; }
.msg-body p:last-child { margin-bottom:0; }
.msg-body strong { font-weight:600; }
.msg-body h3 { font-size:1rem; font-weight:600; margin:1.2rem 0 0.5rem; }
.msg-body ul,.msg-body ol { margin:0.5rem 0 0.8rem 1.5rem; }
.msg-body li { margin-bottom:0.3rem; }
code { font-family:var(--font-mono); font-size:0.85em;
  background:rgba(0,0,0,0.05); padding:0.15em 0.4em; border-radius:4px; }
pre { background:var(--bg-code); border-radius:8px; padding:1rem 1.25rem;
  overflow-x:auto; margin:0.75rem 0; }
pre code { background:none; padding:0; color:var(--text-code);
  font-size:0.82rem; line-height:1.65; }
.encrier-table { width:100%; border-collapse:collapse; font-size:0.85rem; margin:0.8rem 0; }
.encrier-table th { background:var(--accent-asst); color:#fff;
  padding:0.5rem 0.75rem; text-align:left; font-weight:500; }
.encrier-table td { padding:0.5rem 0.75rem; border-bottom:1px solid var(--border); }
.encrier-file-badge { display:inline-flex; align-items:center; gap:0.4rem;
  background:var(--accent-file-bg); color:var(--accent-file);
  padding:0.35rem 0.8rem; border-radius:6px; font-size:0.8rem;
  font-weight:500; margin:0.5rem 0; }
.encrier-widget-frame { width:100%; border:1px solid var(--border);
  border-radius:10px; min-height:400px; margin:1rem 0; background:#fff; }
.encrier-widget-placeholder { border:1px dashed var(--border); border-radius:10px;
  padding:2rem; text-align:center; margin:1rem 0; background:rgba(0,0,0,0.015); }
.encrier-wp-icon { font-size:1.5rem; color:var(--text-muted); margin-bottom:0.5rem; }
.encrier-wp-title { font-weight:600; font-size:0.9rem; margin-bottom:0.25rem; }
.encrier-wp-note { font-size:0.8rem; color:var(--text-muted); }
.encrier-divider { text-align:center; padding:1.5rem 0; position:relative; }
.encrier-divider::before { content:''; position:absolute; top:50%; left:0; right:0;
  border-top:1px solid var(--border); }
.encrier-divider span { background:var(--bg-page); padding:0 1rem; position:relative;
  font-size:0.7rem; font-weight:600; text-transform:uppercase;
  letter-spacing:0.12em; color:var(--text-muted); }
.encrier-condensed-notice { font-size:0.8rem; font-style:italic;
  color:var(--text-muted); margin-top:0.75rem; padding-top:0.5rem;
  border-top:1px dashed var(--border); }
.encrier-footer { text-align:center; padding:2rem; color:var(--text-muted);
  font-size:0.75rem; border-top:1px solid var(--border); }

/* Share bar */
#encrier-share-bar { position:sticky; top:0; z-index:9999;
  display:flex; align-items:center; justify-content:space-between;
  padding:0.6rem 1.5rem; background:#1a1612; }
.encrier-share-wordmark { font-family:var(--font-display);
  font-style:italic; font-size:1rem; color:#e2dfdb;
  letter-spacing:0.02em; }
.encrier-share-actions { display:flex; align-items:center; gap:0.75rem; }
#encrier-share-btn { background:#f6f3ed; color:#1a1612; border:none;
  padding:0.45rem 1.4rem; border-radius:6px; font-size:0.85rem;
  font-weight:600; cursor:pointer; font-family:var(--font-body); transition:all 0.15s; }
#encrier-share-btn:hover { background:#ffffff; }
#encrier-share-btn:disabled { opacity:0.7; cursor:default; }
#encrier-share-status { font-size:0.75rem; }
#encrier-share-status a { color:#a0c4a8; text-decoration:none; }
#encrier-share-status a:hover { text-decoration:underline; }

Share bar

Every save state includes a Share bar as the first element inside the body tag. The bar allows one-click publishing to encrier.com, where the conversation becomes a live, interactive page.

The Share bar HTML:

<div id="encrier-share-bar">
  <span class="encrier-share-wordmark">encrier</span>
  <div class="encrier-share-actions">
    <button id="encrier-share-btn" onclick="encrierPublish()">Share</button>
    <div id="encrier-share-status"></div>
  </div>
</div>

<form id="encrier-push-form" method="POST"
  action="https://encrier.com/api/push" target="_blank"
  enctype="multipart/form-data" style="display:none">
  <textarea name="html" id="encrier-push-payload"></textarea>
</form>

The Share bar script, placed before the closing body tag:

<script id="encrier-share-script">
function encrierPublish() {
  var btn = document.getElementById('encrier-share-btn');
  var status = document.getElementById('encrier-share-status');
  btn.textContent = 'Publishing...';
  btn.disabled = true;
  var clone = document.documentElement.cloneNode(true);
  ['#encrier-share-bar','#encrier-push-form','#encrier-share-script']
    .forEach(function(sel) {
      var el = clone.querySelector(sel);
      if (el) el.parentNode.removeChild(el);
    });
  var html = '<!DOCTYPE html>\n' + clone.outerHTML;
  var file = new File([html], 'conversation.html', {type:'text/html'});
  var fd = new FormData();
  fd.append('file', file);
  fetch('https://encrier.com/api/upload', {method:'POST', body:fd})
    .then(function(r) { return r.json(); })
    .then(function(data) {
      if (data.url) {
        btn.textContent = 'Published \u2713';
        btn.style.background = '#2d6b4f';
        btn.style.color = '#fff';
        status.innerHTML = '<a href="'+data.url+'" target="_blank">'+data.url+'</a>';
        window.open(data.url, '_blank');
      } else { throw new Error('no url'); }
    })
    .catch(function() {
      document.getElementById('encrier-push-payload').value = html;
      document.getElementById('encrier-push-form').submit();
      setTimeout(function() {
        btn.textContent = 'Published \u2713';
        btn.style.background = '#2d6b4f';
        btn.style.color = '#fff';
        status.textContent = 'Opened in new tab';
      }, 800);
    });
}
</script>
The dual-path approach (fetch with form fallback) exists because files opened from local file:// URLs cannot make cross-origin fetch requests. The hidden form bypasses this limitation since HTML forms can POST cross-origin without CORS.

Pre-flight assessment

Before generating a save state, a brief assessment is performed: message count, any widgets or uploaded files that need special handling, a proposed filename following the pattern [topic]-conversation-[date].html, and whether condensation is needed for conversations exceeding ~30 messages.

Error recovery

Hard rules