# Project export: Lens

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Turn any article or URL into core insights, a market angle, and a ready-to-post X thread, a 3-node GPT-5.6 pipeline built with Codex.
- Devpost: https://devpost.com/software/lens-cbjvix
- GitHub: https://github.com/diudiu626/lens
- Video: https://www.youtube.com/embed/SzwgBDIFKxI?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — diudiu626 (3 commits)

## Devpost submission (written by the team)

### Inspiration

Honestly, this started from my own problem. I read a lot: newsletters, articles, threads, and I'd constantly copy things into ChatGPT, ask a few questions, and try to piece together a takeaway by hand. It was messy and I lost most of what I read. The bottleneck was never reading; it was turning what I read into a clear point of view I could actually use or post. I come from a marketing analytics background, and the part I kept wishing I could automate wasn't the writing, it was the analysis: what actually matters here, and who cares. So I wanted to see if that specific lens could become an AI pipeline, instead of just another "summarize this" tool.

### What it does

Lens takes an article (via URL or pasted text) and runs it through three chained GPT-5.6 nodes, each shown as a live progress step: Context Parsing : pulls out the topic, source type, key claims, and background Core Insights + Market Angle : analyzes it through a marketing/audience lens to surface what matters and to whom X Thread Generation : turns that analysis into a ready-to-post thread Output shows up as three cards: Core Insights, Market Angle, X Thread, each with one-click copy. Most content-repurposing tools jump straight from summary to draft, which is why they feel generic. The whole point of Lens is that middle step: it decides what's worth saying, and to whom, before writing anything. How I built it I built this entirely with Codex, and honestly the biggest thing that made it work was scoping ruthlessly. I locked a tiny MVP up front so I wouldn't spiral into feature creep on a tight timeline: Scaffolded the Vite + React app and verified the build Wrote the 3-node pipeline (server/pipeline.js) with structured JSON output so each node reliably feeds the next Built the full dark-theme UI with live per-step progress states and copy-to-clipboard cards Added URL extraction (via Mozilla Readability) as an enhancement after the core paste-text flow was working end to end Kept the OpenAI API key server-side throughout The key calls I made: a 3-node pipeline instead of a multi-agent framework (reliability over ambition, given the time), making the marketing-analysis node the actual differentiator; and forcing structured JSON over free-text parsing so the chain wouldn't quietly break between steps. Challenges I ran into The chained calls were finicky at first, a small schema mismatch in an early node would cascade and break the next one. Locking every node to a strict JSON schema fixed it. URL extraction was the other one: it's easy on clean sources like Wikipedia, but real-world blogs and newsletters don't always parse, so I added a fallback that just asks the user to paste the text instead of failing silently. What I learned That reliable multi-step AI pipelines come down to the contracts between steps far more than clever prompting, and that scoping small (3 nodes, one input flow, no auth or database) is exactly what let me ship something complete instead of something ambitious and broken. As someone still fairly new to building like this, that lesson stuck.

### What's next

More output formats (Instagram, TikTok, LinkedIn), YouTube transcript input, and a lightweight history so I can revisit past analyses.

## README (from the GitHub repository)

# Lens — Content Intelligence Pipeline

**Turn any article or URL into core insights, a market angle, and a ready-to-post X thread.**

Lens is a 3-node GPT-5.6 pipeline that takes raw text or a URL and runs it through chained reasoning steps — parsing → analysis → content generation — so knowledge workers and content creators can go from "too much information" to a clear, publishable point of view in seconds.

Built for **OpenAI Build Week 2026** · Track: **Work and productivity**

---

## What it does

Paste an article (or drop in a URL) and Lens runs it through three sequential GPT-5.6 nodes, each visible as a live progress step:

1. **Context Parsing** — extracts the topic, source type, key claims, and background context
2. **Core Insights + Market Angle** — surfaces the insights that matter, identifies who they matter to, and finds the contrarian/practical angle worth highlighting (this is the differentiator: most tools skip straight to writing and lose the analysis layer)
3. **X Thread Generation** — turns the analysis into a scroll-stopping, ready-to-publish thread

The output renders as three cards — Core Insights, Market Angle, X Thread — each with one-click copy.

---

## Why this design

Most "article-to-content" tools jump from source → draft and produce generic summaries. Lens deliberately inserts an **analysis layer** in the middle, applying a marketing/audience-strategy lens before any content is written. The result reads more intentional and less like an average-of-the-internet summary.

The chained-node architecture (rather than a single prompt) also makes the reasoning transparent and each stage independently improvable.

---

## Tech stack

- **Frontend:** React + Vite (single page)
- **Backend:** Node/Express API, keeps the OpenAI key server-side
- **Model:** OpenAI **GPT-5.6** via the Responses API with **structured JSON output** for reliable, parseable results at every node
- **URL extraction:** `@mozilla/readability` + `jsdom` to pull clean article text from a link
- **Built with:** OpenAI **Codex**

---

## How Codex and GPT-5.6 were used

**GPT-5.6** is the reasoning engine behind all three nodes. Each node uses a distinct system prompt and structured JSON schema so its output can be reliably passed into the next node — parsing feeds analysis, analysis feeds thread generation.

**Codex** accelerated the entire build:
- Scaffolded the Vite + React project from scratch and verified it built
- Wrote the 3-node pipeline (`server/pipeline.js`) and wired the sequential API calls
- Built the full dark-theme UI with live per-step progress states and copy buttons
- Added URL extraction as a later enhancement, keeping paste-text intact, and handled the failure/fallback case
- Handled cross-cutting concerns (server-side key handling, `.gitignore`, `.env.example`)

**Key decisions** (where human direction shaped the build):
- Locking scope to a 3-node pipeline instead of a multi-agent framework, to keep it reliable and demoable
- Inserting the marketing/audience analysis as a dedicated middle node — the core idea that differentiates Lens
- Prioritizing structured JSON output over free-text parsing for reliability between nodes
- Adding URL support as a bonus only after the core paste-text flow was verified end to end

---

## Run it locally

**Prerequisites:** Node.js installed, and an OpenAI API key with credit.

1. Clone the repo:
   ```
   git clone https://github.com/diudiu626/lens.git
   cd lens
   ```

2. Install dependencies:
   ```
   npm install
   ```

3. Create a `.env` file in the project root (see `.env.example`):
   ```
   OPENAI_API_KEY=your_api_key_here
   ```

4. Start the app (runs Vite + the API server together):
   ```
   npm run dev
   ```

5. Open `http://localhost:5173`

**Try it:** Click **"Try an example"** to auto-fill a sample article, or paste your own text / a URL, then hit **Run pipeline**.

---

## Sample input

Any article, essay, transcript, or newsletter works well. The built-in example is a short piece on AI content workflows. URLs from most standard blogs, news sites, and newsletters extract cleanly; if a page can't be parsed, Lens tells you to paste the text manually.


## Detected evidence (automated analysis)

Indexed codebase: 12 recognized source files, 27 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (17 of 17)

```
.env.example
.gitignore
.oxlintrc.json
index.html
LICENSE
package.json
README.md
server/api.js
server/extractArticle.js
server/pipeline.js
server/test-node1.js
server/test-pipeline.js
src/App.css
src/App.jsx
src/index.css
src/main.jsx
vite.config.js
```

### Dependencies

- package.json: @mozilla/readability@^0.6.0, @types/react@^19.2.17, @types/react-dom@^19.2.3, @vitejs/plugin-react@^6.0.3, concurrently@^10.0.3, dotenv@^17.4.2, express@^5.2.1, jsdom@^29.1.1, openai@^6.48.0, oxlint@^1.71.0, react@^19.2.7, react-dom@^19.2.7, vite@^8.1.1

### Recent commits (newest first)

- Create LICENSE
- Update README.md
- Lens - OpenAI Build Week submission

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
    "name": "lens",
    "private": true,
    "version": "0.0.0",
    "type": "module",
    "scripts": {
        "dev": "concurrently \"vite\" \"node --watch server/api.js\"",
        "build": "vite build",
        "lint": "oxlint",
        "preview": "vite preview",
        "server": "node server/api.js"
    },
    "dependencies": {
        "@mozilla/readability": "^0.6.0",
        "concurrently": "^10.0.3",
        "dotenv": "^17.4.2",
        "express": "^5.2.1",
        "jsdom": "^29.1.1",
        "openai": "^6.48.0",
        "react": "^19.2.7",
        "react-dom": "^19.2.7"
    },
    "devDependencies": {
        "@types/react": "^19.2.17",
        "@types/react-dom": "^19.2.3",
        "@vitejs/plugin-react": "^6.0.3",
        "oxlint": "^1.71.0",
        "vite": "^8.1.1"
    }
}

```

### src/main.jsx

```javascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### src/App.jsx

```javascript
import { useState } from 'react'
import './App.css'

const steps = [['parsing', 'Parsing...'], ['analyzing', 'Analyzing...'], ['writing', 'Writing thread...']]

const exampleArticle = `AI content teams are increasingly moving away from one-off prompts and toward repeatable workflows. Instead of asking a model to write a post from scratch, teams first capture the source material, pull out the important claims, and decide which audience needs to hear them.

That middle analysis step is becoming the differentiator. It connects a dense article or interview to a practical market angle, helping a team decide what to emphasize before it starts drafting. The result is content that sounds more intentional and less like a generic summary.

For small marketing teams, this workflow can turn one strong source into several focused assets: a social thread, a newsletter section, a sales enablement brief, or a customer education post. The goal is not more content for its own sake; it is a clearer point of view delivered in the right format.

The best systems still leave room for human judgment. A workflow can make research, analysis, and drafting faster, while the final editorial decision stays with the person who understands the brand and audience.`

async function post(path, body) {
  const response = await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
  const payload = await response.json()
  if (!response.ok) throw new Error(payload.error || 'Something went wrong.')
  return payload
}

function CopyButton({ value }) {
  const [copied, setCopied] = useState(false)
  const copy = async () => { await navigator.clipboard.writeText(value); setCopied(true); window.setTimeout(() => setCopied(false), 1600) }
  return <button className="copy-button" onClick={copy}>{copied ? 'Copied' : 'Copy'}</button>
}

function App() {
  const [url, setUrl] = useState('')
  const [text, setText] = useState('')
  const [status, setStatus] = useState('idle')
  const [results, setResults] = useState(null)
  const [error, setError] = useState('')
  const running = !['idle', 'complete', 'error'].includes(status)

  function tryExample() {
    setUrl('')
    setText(exampleArticle)
    setError('')
  }

  async function handleSubmit(event) {
    event.preventDefault()
    if ((!url.trim() && !text.trim()) || running) return
    setError(''); setResults(null)
    try {
      setStatus('parsing')
      const parsed = await post('/api/parse', { text, url })
      setStatus('analyzing')
      const analysis = await post('/api/analyze', { parsed })
      setStatus('writing')
      const thread = await post('/api/thread', { analysis })
      setResults({ parsed, analysis, thread }); setStatus('complete')
    } catch (requestError) { setError(requestError.message); setStatus('error') }
  }

  const threadText = results ? [results.thread.hook, ...results.thread.thread].join('\n\n') : ''

  return <main className="page-shell">
    <section className="hero">
      <p className="eyebrow">Content intelligence pipeline</p>
      <h1>Turn raw text into a clear point of view.</h1>
      <p className="lede">Paste an article or add its URL. Get core insights, a market angle, and an X thread ready to refine.</p>
    </section>

    <form className="composer" onSubmit={handleSubmit}>
      <div className="field-heading"><label htmlFor="source-url">Article URL <span>(optional — takes priority)</span></label><button className="example-button" type="button" onClick={tryExample} disabled={running}>Try an example</button></div>
      <input id="source-url" type="url" value={url} onChange={(event) => setUrl(event.target.value)} placeholder="https://example.com/article" disabled={running} />
      <label htmlFor="source-text">Or paste source material</label>
      <textarea id="source-text" value={text} onChange={(event) => setText(event.target.value)} placeholder="Paste an article, transcript, or notes here..." rows="11" disabled={running} />
      <div className="form-footer"><span>{url.trim() ? 'URL will be used' : `${text.length.toLocaleString()} characters`}</span><button className="primary-button" type="submit" disabled={(!url.trim() && !text.trim()) || running}>{running ? 'Working…' : 'Run pipeline'}</button></div>
    </form>

    {(running || status === 'complete') && <section className="progress" aria-live="polite">{steps.map(([key, label], index) => { const active = status === key; const done = status === 'complete' || steps.findIndex(([item]) => item === status) > index; return <div className={`step ${active ? 'active' : ''} ${done ? 'done' : ''}`} key={key}><span className="step-marker">{done ? '✓' : index + 1}</span>{active ? label : label.replace('...', '')}</div> })}</section>}
    {error && <p className="error-message" role="alert">{error}</p>}

    {results && <section className="results" aria-label="Pipeline results">
      <article className="result-card">
        <div className="card-header"><div><p className="card-kicker">1 · Core Insights</p><h2>What stands out</h2></div><CopyButton value={results.analysis.core_insights.join('\n')} /></div>
        <ul>{results.analysis.core_insights.map((insight) => <li key={insight}>{insight}</li>)}</ul>
        <div className="detail-row"><span className="section-label">Why it matters</span><p>{results.analysis.why_it_matters}</p></div>
      </article>
      <article className="result-card">
        <div className="card-header"><div><p className="card-kicker">2 · Market Angle</p><h2>The audience lens</h2></div><CopyButton value={`${results.analysis.market_angle}\n\nAudience: ${results.analysis.target_audience}\n\nApplication: ${results.analysis.opportunity_or_application}`} /></div>
        <p className="feature-copy">{results.analysis.market_angle}</p>
        <div className="detail-row"><span className="section-label">Audience</span><p>{results.analysis.target_audience}</p></div>
        <div className="detail-row"><span className=
[truncated — 719 more characters]
```

### vite.config.js

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': 'http://localhost:8787',
    },
  },
})
```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>lens</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

```

### src/index.css

```css
:root { background: #0a0c12; }
body { min-height: 100vh; }
```

### server/api.js

```javascript
import express from 'express'
import { extractArticleText } from './extractArticle.js'
import { analyzeContent, parseContent, writeThread } from './pipeline.js'

const app = express()
const port = process.env.PORT || 8787
app.use(express.json({ limit: '1mb' }))

app.post('/api/parse', async (req, res) => {
  const url = typeof req.body?.url === 'string' ? req.body.url.trim() : ''
  const text = typeof req.body?.text === 'string' ? req.body.text.trim() : ''
  if (!url && !text) return res.status(400).json({ error: 'Provide a URL or paste text to continue.' })

  try {
    const sourceText = url ? await extractArticleText(url) : text
    res.json(await parseContent(sourceText))
  } catch (error) {
    res.status(error.status || 422).json({ error: error.message || 'Parsing failed.' })
  }
})

app.post('/api/analyze', async (req, res) => {
  if (!req.body?.parsed) return res.status(400).json({ error: 'parsed content is required.' })
  try { res.json(await analyzeContent(req.body.parsed)) }
  catch (error) { res.status(error.status || 500).json({ error: error.message || 'Analysis failed.' }) }
})

app.post('/api/thread', async (req, res) => {
  if (!req.body?.analysis) return res.status(400).json({ error: 'analysis is required.' })
  try { res.json(await writeThread(req.body.analysis)) }
  catch (error) { res.status(error.status || 500).json({ error: error.message || 'Thread generation failed.' }) }
})

app.listen(port, () => console.log(`Pipeline API listening on http://localhost:${port}`))
```

### server/test-pipeline.js

```javascript
import { analyzeContent, parseContent, writeThread } from './pipeline.js'

const testArticle = `In March 2024, the European Parliament approved the Artificial Intelligence Act, a wide-ranging law intended to regulate AI systems according to the risks they pose. The rules prohibit certain uses, including social scoring and some forms of real-time remote biometric identification in public spaces, while allowing limited exceptions for law enforcement.

The law also places new obligations on developers of general-purpose AI models. Providers must prepare technical documentation, follow copyright requirements, and publish summaries of training content. Models judged to create systemic risks face additional evaluation, incident-reporting, and cybersecurity requirements.

Most provisions will take effect gradually after the regulation enters force. The European Commission and national authorities will oversee enforcement, with penalties that can reach a percentage of a company’s worldwide annual revenue. Supporters describe the act as the first comprehensive AI law by a major economic bloc, while companies are preparing for its compliance deadlines.`

const node1 = await parseContent(testArticle)
const node2 = await analyzeContent(node1)
const node3 = await writeThread(node2)

console.log('Node 1 — Context Parsing')
console.log(JSON.stringify(node1, null, 2))
console.log('\nNode 2 — Audience & Market Analysis')
console.log(JSON.stringify(node2, null, 2))
console.log('\nNode 3 — X/Twitter Thread')
console.log(JSON.stringify(node3, null, 2))
```

### server/extractArticle.js

```javascript
import { Readability } from '@mozilla/readability'
import { JSDOM } from 'jsdom'

const MIN_ARTICLE_LENGTH = 280

export async function extractArticleText(rawUrl) {
  let url
  try {
    url = new URL(rawUrl)
  } catch {
    throw new Error('That URL is not valid. Please paste the article text manually instead.')
  }

  if (!['http:', 'https:'].includes(url.protocol)) {
    throw new Error('Only http and https URLs are supported. Please paste the article text manually instead.')
  }

  try {
    const response = await fetch(url, {
      headers: { Accept: 'text/html,application/xhtml+xml', 'User-Agent': 'Lens content pipeline/1.0' },
      signal: AbortSignal.timeout(15_000),
    })
    if (!response.ok) throw new Error(`The page returned HTTP ${response.status}.`)

    const contentType = response.headers.get('content-type') || ''
    if (!contentType.includes('text/html') && !contentType.includes('application/xhtml+xml')) {
      throw new Error('The URL did not return an HTML article page.')
    }

    const html = await response.text()
    const dom = new JSDOM(html, { url: url.href })
    const article = new Readability(dom.window.document).parse()
    const text = article?.textContent?.replace(/\s+/g, ' ').trim() || ''
    if (text.length < MIN_ARTICLE_LENGTH) throw new Error('We could not extract enough article text from that page.')
    return text
  } catch (error) {
    const detail = error.name === 'TimeoutError' ? 'The page took too long to load.' : error.message
    throw new Error(`${detail} Please paste the text manually instead.`)
  }
}
```

### server/test-node1.js

```javascript
import 'dotenv/config'
import OpenAI from 'openai'

const systemPrompt = `You are a content analyst. Given raw text or article content, extract the essential structure. Be concise and factual — no opinions yet.

Output valid JSON only, no markdown, no preamble:
{
  "topic": "one-line topic",
  "source_type": "article/essay/transcript/etc",
  "key_claims": ["claim 1", "claim 2", "claim 3"],
  "context": "2-3 sentence background needed to understand this piece"
}`

const testArticle = `In March 2024, the European Parliament approved the Artificial Intelligence Act, a wide-ranging law intended to regulate AI systems according to the risks they pose. The rules prohibit certain uses, including social scoring and some forms of real-time remote biometric identification in public spaces, while allowing limited exceptions for law enforcement.

The law also places new obligations on developers of general-purpose AI models. Providers must prepare technical documentation, follow copyright requirements, and publish summaries of training content. Models judged to create systemic risks face additional evaluation, incident-reporting, and cybersecurity requirements.

Most provisions will take effect gradually after the regulation enters force. The European Commission and national authorities will oversee enforcement, with penalties that can reach a percentage of a company’s worldwide annual revenue. Supporters describe the act as the first comprehensive AI law by a major economic bloc, while companies are preparing for its compliance deadlines.`

const schema = {
  type: 'object',
  properties: {
    topic: { type: 'string' },
    source_type: { type: 'string' },
    key_claims: {
      type: 'array',
      items: { type: 'string' },
    },
    context: { type: 'string' },
  },
  required: ['topic', 'source_type', 'key_claims', 'context'],
  additionalProperties: false,
}

if (!process.env.OPENAI_API_KEY) {
  throw new Error('OPENAI_API_KEY is missing. Add it to the project root .env file.')
}

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

const response = await client.responses.create({
  model: 'gpt-5.6',
  input: [
    { role: 'system', content: systemPrompt },
    { role: 'user', content: testArticle },
  ],
  text: {
    format: {
      type: 'json_schema',
      name: 'content_analysis',
      schema,
      strict: true,
    },
  },
})

console.log(JSON.stringify(JSON.parse(response.output_text), null, 2))
```

[2 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]