AI Smart Paste

Drop any shape of contact / lead / row data into a text area - CSV, TSV, JSON, vCard, Markdown table, an email signature block, or a single line of prose - and the parser maps it into typed rows your grid can insert or merge.

This is the demo your sales / customer-success / RevOps users want. Their day already has them pasting from Salesforce, Slack, Excel, business cards, and signature blocks; the parser absorbs the chaos and the grid renders the cleaned result.

What the parser handles

Six input formats are detected automatically, tried in this order:

Format Example trigger Notes
vCard BEGIN:VCARD RFC 6350. iOS / macOS / Google Contacts export.
Markdown table Lines starting/ending with ` , with a ---` separator
JSON Input starts with [ or { Any field names; multi-language matched.
CSV / TSV / PSV Consistent tab / comma / semicolon / pipe delimiter Multi-language headers (Nom, Téléphone etc).
Signature blocks Multi-line blocks separated by blank lines containing an email Name <email> form, "Role, Company" lines.
Free-form prose Anything else, one line per record Extracts by signal (email regex, phone regex).

On top of detection, every parsed row goes through three normalization passes before the preview panel renders it:

  1. Email typo correction - common domain typos (gmial.com, gmail.con, gosling.con) are auto-fixed and surfaced as an info note on the row.
  2. Phone normalization - any phone is rewritten to a consistent +CC AAA BBB CCCC shape (US, UK, DE, FR, JP recognized; 10-digit bare numbers assumed North American).
  3. Name cleaning - titles (Dr., Prof., Mr.) and suffixes (Jr., III, PhD) are stripped; "Last, First" is rewritten to "First Last".

Duplicate detection runs last: rows with the same normalized email collapse into one, with missing fields merged from the duplicate.

Complete drop-in example

A minimal contact list with the Smart Paste panel above the grid. Paste any of the formats below into the textarea and click Parse with AI.

<script lang="ts">
  import {
    SvGrid,
    tableFeatures,
    rowSortingFeature,
    rowSelectionFeature,
    type ColumnDef,
    type SvGridApi,
  } from 'sv-grid-community'

  type Contact = {
    id: string
    fullName: string
    email: string
    company: string
    role: string
    phone: string
  }

  let rows = $state<Contact[]>([
    { id: 'c1', fullName: 'Ada Lovelace',   email: '[email protected]', company: 'Analytic Engine',  role: 'Founder',    phone: '+44 20 1234 5678' },
    { id: 'c2', fullName: 'Linus Torvalds', email: '[email protected]',    company: 'Linux Foundation', role: 'Maintainer', phone: '+1 503 555 0101' },
  ])

  const features = tableFeatures({ rowSortingFeature, rowSelectionFeature })
  let api = $state<SvGridApi<typeof features, Contact> | null>(null)

  let pasteText = $state('')
  let parsed = $state<Contact[]>([])

  // The real parser is bundled in demo 75. This stub does just CSV +
  // semicolon detection; swap with `assistant(text)` from the demo to
  // get the full feature set.
  async function parse(text: string): Promise<Contact[]> {
    const delim = text.includes('\t') ? '\t'
                : text.includes(';')  ? ';'
                : ','
    return text.trim().split('\n').map((line, i) => {
      const cells = line.split(delim).map((c) => c.trim())
      return {
        id: `paste-${i}`,
        fullName: cells[0] ?? '',
        email:    cells[1] ?? '',
        company:  cells[2] ?? '',
        role:     cells[3] ?? '',
        phone:    cells[4] ?? '',
      }
    })
  }

  async function runParse() {
    parsed = await parse(pasteText)
  }

  function applyAll() {
    if (!api || parsed.length === 0) return
    api.addRows(parsed)
    parsed = []
    pasteText = ''
  }

  const columns: ColumnDef<typeof features, Contact>[] = [
    { field: 'fullName', header: 'Name',    editorType: 'text', width: 180 },
    { field: 'email',    header: 'Email',   editorType: 'text', width: 220 },
    { field: 'company',  header: 'Company', editorType: 'text', width: 180 },
    { field: 'role',     header: 'Role',    editorType: 'text', width: 160 },
    { field: 'phone',    header: 'Phone',   editorType: 'text', width: 160 },
  ]
</script>

<section style="display: flex; flex-direction: column; gap: 12px; height: 100%;">
  <textarea
    bind:value={pasteText}
    placeholder="Paste CSV / TSV / JSON / vCard / Markdown / signature blocks here…"
    rows="4"
    style="width: 100%; font-family: ui-monospace, Menlo, monospace; padding: 10px; border-radius: 8px; border: 1px solid #cbd5e1;"
  ></textarea>

  <div style="display: flex; gap: 8px;">
    <button onclick={runParse}>✨ Parse with AI</button>
    <button onclick={applyAll} disabled={parsed.length === 0}>
      Apply {parsed.length} row{parsed.length === 1 ? '' : 's'}
    </button>
  </div>

  {#if parsed.length > 0}
    <div style="border: 1px solid #cbd5e1; border-radius: 8px; padding: 10px;">
      <strong>Preview:</strong>
      <ul>
        {#each parsed as p (p.id)}
          <li>{p.fullName} · {p.email} · {p.company}</li>
        {/each}
      </ul>
    </div>
  {/if}

  <div style="flex: 1; min-height: 0;">
    <SvGrid
      data={rows}
      columns={columns}
      features={features}
      containerHeight="100%"
      onApiReady={(next) => (api = next)}
    />
  </div>
</section>

For the full parser (vCard, Markdown table, signature blocks, multi-language headers, typo correction, phone normalization, de-duplication), read the assistant() implementation in demo 75 and copy it verbatim - the parser is 350 lines of pure TypeScript with no library dependencies.

Try these input formats

Each one runs through the parser and produces 2-3 rows in the preview panel.

vCard

BEGIN:VCARD
VERSION:3.0
FN:Brendan Eich
ORG:Brave Software
TITLE:CEO
EMAIL;TYPE=WORK:[email protected]
TEL;TYPE=CELL:+1 415 555 0188
END:VCARD
BEGIN:VCARD
VERSION:3.0
FN:Anders Hejlsberg
ORG:Microsoft
TITLE:Technical Fellow
EMAIL;TYPE=WORK:[email protected]
TEL;TYPE=WORK:+1 425 555 0177
END:VCARD

Markdown table with multi-language headers

| Name | Email | Company | Rol | Téléphone |
|---|---|---|---|---|
| Grace Hopper | [email protected] | US Navy | Rear Admiral | +1 202 555 0133 |
| Ken Thompson | [email protected] | Bell Labs | Researcher | +1 908 555 0124 |

Email signature blocks

Donald E. Knuth <[email protected]>
Professor Emeritus, Stanford University
Phone: (650) 555-0111

Barbara Liskov <[email protected]>
Institute Professor at MIT CSAIL
Cell: +1.617.555.0142

Messy real-world paste

Title prefixes, "Last, First" inversion, email typo, and varied phone formats - the parser cleans every field:

Dr. Margaret Rhodes; [email protected]; Rhodes Capital LLC; Managing Partner; (212) 555-0188
Wirth, Niklaus; [email protected]; ETH Zurich; Professor Emeritus; +41 44 555 0166
"James Gosling" <[email protected]>; Amazon Web Services; Distinguished Engineer; 1-415-555-0177

After parsing:

Swapping in a real LLM

The bundled assistant() is fully deterministic and runs in the browser. To route through a real model instead - GPT, Claude, Gemini, Llama on your infra - replace the assistant() body with:

async function assistant(text: string): Promise<{ rows: ParsedRow[]; log: string[] }> {
  const res = await fetch('/api/smart-paste', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ text }),
  })
  if (!res.ok) throw new Error(`Parser failed: ${res.status}`)
  return res.json() as Promise<{ rows: ParsedRow[]; log: string[] }>
}

On the server side, the model receives the raw paste and a schema for ParsedRow, and returns the same shape. Useful when you need:

The bundled parser stays the deterministic fallback - call it client-side when the network call fails or for the 90% of cases that don't need a model.

See also