---
format: typebulb/v1
name: Dr. Sous
---

**code.tsx**

```tsx
import React, { useState, useMemo, useRef, useEffect } from "react";
import { createRoot } from "react-dom/client";
import { ChefHat, Play, Timer, List, Clock } from "lucide-react";

interface Ingredient {
  item: string;
  amount: string;
}

interface RecipeStep {
  id: string;
  task: string;
  details: string;
  startTime: number;
  duration: number;
  type: "prep" | "cook" | "rest" | "plating";
}

interface RecipeTimeline {
  recipeTitle: string;
  ingredients: Ingredient[];
  totalTimeMinutes: number;
  steps: RecipeStep[];
}

const TYPE_COLORS = {
  prep: "#f59e0b",
  cook: "#ef4444",
  rest: "#3b82f6",
  plating: "#10b981",
};

function App() {
  const [timeline, setTimeline] = useState<RecipeTimeline | null>(tb.insight<RecipeTimeline>());
  const [loading, setLoading] = useState(false);
  const [serveTime, setServeTime] = useState<string>("18:00");
  const detailRefs = useRef<Map<string, HTMLDivElement>>(new Map());
  const miniGanttRef = useRef<HTMLDivElement>(null);

  const handleInfer = async () => {
    setLoading(true);
    try {
      const result = await tb.infer<RecipeTimeline>();
      if (result) setTimeline(result);
    } finally {
      setLoading(false);
    }
  };

  // Calculate clock time from minutes-from-start
  const getClockTime = (minutesFromStart: number): string => {
    if (!timeline) return "";
    const [h, m] = serveTime.split(":").map(Number);
    const serveMins = h * 60 + m;
    const startMins = serveMins - timeline.totalTimeMinutes;
    const actualMins = startMins + minutesFromStart;
    const hours = Math.floor(((actualMins % 1440) + 1440) % 1440 / 60);
    const mins = ((actualMins % 60) + 60) % 60;
    const ampm = hours >= 12 ? "pm" : "am";
    const h12 = hours % 12 || 12;
    return `${h12}:${mins.toString().padStart(2, "0")}${ampm}`;
  };

  // Assign rows to steps for mini-gantt stacking
  const stepsWithRows = useMemo(() => {
    if (!timeline) return [];
    const sorted = [...timeline.steps].sort((a, b) => a.startTime - b.startTime);
    const rows: { end: number }[] = [];
    return sorted.map(step => {
      const stepEnd = step.startTime + step.duration;
      let row = rows.findIndex(r => r.end <= step.startTime);
      if (row === -1) {
        row = rows.length;
        rows.push({ end: stepEnd });
      } else {
        rows[row].end = stepEnd;
      }
      return { ...step, row };
    });
  }, [timeline]);

  const maxRow = useMemo(() => Math.max(0, ...stepsWithRows.map(s => s.row)), [stepsWithRows]);

  // Group steps for detail view
  const groupedSteps = useMemo(() => {
    if (!timeline) return [];
    const sorted = [...timeline.steps].sort((a, b) => a.startTime - b.startTime);
    const groups: RecipeStep[][] = [];
    sorted.forEach(step => {
      const lastGroup = groups[groups.length - 1];
      if (lastGroup && step.startTime < Math.max(...lastGroup.map(s => s.startTime + s.duration))) {
        lastGroup.push(step);
      } else {
        groups.push([step]);
      }
    });
    return groups;
  }, [timeline]);

  const scrollToStep = (stepId: string) => {
    const el = detailRefs.current.get(stepId);
    el?.scrollIntoView({ behavior: "smooth", block: "center" });
  };

  const BAR_HEIGHT = 14;
  const ROW_GAP = 3;

  const getPct = (mins: number) => {
    if (!timeline) return 0;
    return (mins / timeline.totalTimeMinutes) * 100;
  };

  const ticks = useMemo(() => {
    if (!timeline) return [];
    const [h, m] = serveTime.split(":").map(Number);
    const serveMins = h * 60 + m;
    const startMins = serveMins - timeline.totalTimeMinutes;
    const firstHourMins = Math.ceil(startMins / 60) * 60;
    const lastHourMins = Math.floor(serveMins / 60) * 60;
    const res = [];
    for (let t = firstHourMins; t <= lastHourMins; t += 60) {
      res.push({
        offset: t - startMins,
        label: ((Math.floor(t / 60) % 12) || 12).toString()
      });
    }
    return res;
  }, [timeline, serveTime]);

  return (
    <div className="container">
      <header className="app-header">
        <div className="header-left">
          <div className="brand-badge">
            <ChefHat size={20} />
            <span>DR. SOUS</span>
          </div>
          <p className="app-tagline">Let Dr. Sous turn any recipe into a cooking timeline.</p>
          <h1 className="recipe-title">
            {timeline?.recipeTitle || "Paste a recipe to begin"}
          </h1>
        </div>
        <div className="header-right">
          {timeline && (
            <div className="serve-time-picker">
              <Clock size={14} />
              <label>Serve at</label>
              <input
                type="time"
                value={serveTime}
                onChange={e => setServeTime(e.target.value)}
              />
            </div>
          )}
          <button onClick={handleInfer} disabled={loading} className="generate-btn">
            {loading ? "Analyzing..." : timeline ? "Switch Recipe" : "Analyze Recipe"}
            <Play size={14} fill="currentColor" />
          </button>
        </div>
      </header>

      {timeline ? (
        <main className="app-main">
          <section className="mini-gantt-section">
            <div className="mini-gantt-header">
              <span className="start-time">{getClockTime(0)}</span>
              <span className="total-time">{Math.floor(timeline.totalTimeMinutes / 60)}h {timeline.totalTimeMinutes % 60}m total</span>
              <span className="end-time">{getClockTime(timeline.totalTimeMinutes)}</span>
            </div>
            <div className="mini-gantt-scroll" ref={miniGanttRef}>
              <div className="mini-gantt-wrapper">
                <div
                  className="mini-gantt"
                  style={{
                    height: (maxRow + 1) * (BAR_HEIGHT + ROW_GAP) + 20,
                  }}
                >
                  {ticks.map((tick, i) => (
                    <div key={i} className="gantt-tick" style={{ left: `${getPct(tick.offset)}%` }}>
                      <span className="tick-label">{tick.label}</span>
                    </div>
                  ))}
                  <div className="gantt-bars-area">
                    {stepsWithRows.map(step => (
                      <div
                        key={step.id}
                        className="mini-bar"
                        title={`${step.task} (${step.duration}m)`}
                        style={{
                          left: `${getPct(step.startTime)}%`,
                          width: `${Math.max(getPct(step.duration), 0.5)}%`,
                          top: step.row * (BAR_HEIGHT + ROW_GAP) + ROW_GAP,
                          height: BAR_HEIGHT,
                          backgroundColor: TYPE_COLORS[step.type],
                        }}
                        onClick={() => scrollToStep(step.id)}
                      />
                    ))}
                  </div>
                </div>
              </div>
            </div>
            <div className="mini-gantt-legend">
              {Object.entries(TYPE_COLORS).map(([type, color]) => (
                <span key={type} className="legend-item">
                  <span className="legend-dot" style={{ backgroundColor: color }} />
                  {type}
                </span>
              ))}
            </div>
          </section>

          {/* Ingredients */}
          <section className="ingredients-section card">
            <div className="card-header"><List size={18} /> <h2>Ingredients</h2></div>
            <ul className="ingredients-grid">
              {timeline.ingredients.map((ing, i) => (
                <li key={i}><span className="amount">{ing.amount}</span> {ing.item}</li>
              ))}
            </ul>
          </section>

          {/* Detailed Timeline */}
          <section className="stream-view">
            {groupedSteps.map((group, groupIdx) => (
              <div key={groupIdx} className="step-group">
                <div className="group-time">
                  <span className="time-label">{getClockTime(group[0].startTime)}</span>
                  <div className="time-line" />
                </div>
                <div className={`group-content ${group.length > 1 ? "parallel" : ""}`}>
                  {group.map(step => (
                    <div
                      key={step.id}
                      ref={el => { if (el) detailRefs.current.set(step.id, el); }}
                      className={`step-card ${step.type}`}
                    >
                      <div className="step-header">
                        <span className="step-title">{step.task}</span>
                        <span className="step-duration">{step.duration}m</span>
                      </div>
                      <p className="step-details">{step.details}</p>
                      <div className="step-footer">
                        <span className="type-pill">{step.type}</span>
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            ))}
            <div className="finish-marker">
              <div className="group-time">
                <span className="time-label">{getClockTime(timeline.totalTimeMinutes)}</span>
              </div>
              <div className="finish-label">
                <div className="finish-dot" />
                <span>Ready to serve!</span>
              </div>
            </div>
          </section>
        </main>
      ) : (
        <div className="welcome-state">
          <Timer size={64} className="empty-icon" />
          <h2>Ready to Cook?</h2>
          <p>Paste a recipe in the Data tab, then click Analyze Recipe.</p>
        </div>
      )}
    </div>
  );
}

const container = document.getElementById("root");
const root = createRoot(container!);
root.render(<App />);

```
**styles.css**

```css
:root {
  --bg-main: #f8fafc;
  --bg-card: #ffffff;
  --text-main: #0f172a;
  --text-muted: #64748b;
  --border: #e2e8f0;
  --accent: #6366f1;
  --btn-text: #ffffff;
  --prep: #f59e0b;
  --cook: #ef4444;
  --rest: #3b82f6;
  --plating: #10b981;
}

html[data-theme="dark"] {
  --bg-main: #0a0a0a;
  --bg-card: #141414;
  --text-main: #f1f5f9;
  --text-muted: #a3a3a3;
  --border: #262626;
  --accent: #818cf8;
  --btn-text: #0a0a0a;
}

body {
  margin: 0;
  background: var(--bg-main);
  color: var(--text-main);
  font-family: "Inter", system-ui, sans-serif;
}

.container {
  max-width: 900px;
  margin: 0 auto;
  padding: 1.5rem;
}

/* Header */
.app-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  flex-wrap: wrap;
  gap: 2rem;
  margin-bottom: 2rem;
  padding-bottom: 1.5rem;
  border-bottom: 1px solid var(--border);
}

.header-left {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

.header-right {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  flex-wrap: wrap;
  justify-content: center;
  width: 100%;
}

.brand-badge {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 1.1rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}

.recipe-title {
  font-size: 1.75rem;
  font-weight: 800;
  margin: 0;
  line-height: 1.15;
  letter-spacing: -0.02em;
}

.app-tagline {
  margin: -0.75rem 0 0;
  font-size: 0.9rem;
  color: var(--text-muted);
  max-width: 400px;
  line-height: 1.4;
}

.serve-time-picker {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.85rem;
  color: var(--text-muted);
}

.serve-time-picker input {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.4rem 0.6rem;
  font-size: 0.85rem;
  color: var(--text-main);
  font-weight: 600;
}

.generate-btn {
  background: var(--accent);
  color: var(--btn-text);
  border: none;
  padding: 0.6rem 1.25rem;
  border-radius: 8px;
  font-weight: 600;
  font-size: 0.85rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
  transition: opacity 0.2s;
}

.generate-btn:hover { opacity: 0.85; }
.generate-btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* Mini Gantt */
.mini-gantt-section {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1rem;
  margin-bottom: 1.5rem;
}

.mini-gantt-header {
  display: flex;
  justify-content: space-between;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 0.5rem;
}

.mini-gantt-header .total-time {
  color: var(--text-main);
}

.mini-gantt-scroll {
  overflow-x: auto;
  overflow-y: hidden;
  margin-bottom: 0.5rem;
}

.mini-gantt-wrapper {
  display: flex;
  flex-direction: column;
  padding: 0 1.5rem;
  width: max-content;
  min-width: 100%;
  box-sizing: border-box;
}

.mini-gantt {
  position: relative;
  border-radius: 6px;
  background: rgba(0,0,0,0.02);
  border: 1px solid var(--border);
  margin-bottom: 1.5rem;
  width: 100%;
  min-width: 650px;
  box-sizing: border-box;
}

.gantt-tick {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--border);
  z-index: 1;
}

.tick-label {
  position: absolute;
  bottom: -18px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.65rem;
  font-weight: 700;
  color: var(--text-muted);
}

.gantt-bars-area {
  position: relative;
  height: 100%;
  z-index: 2;
}

.mini-bar {
  position: absolute;
  border-radius: 3px;
  cursor: pointer;
  opacity: 0.9;
  transition: opacity 0.15s, transform 0.15s;
}

.mini-bar:hover {
  opacity: 1;
  transform: scaleY(1.2);
  z-index: 10;
}

.mini-gantt-legend {
  display: flex;
  gap: 1rem;
  margin-top: 0.75rem;
  font-size: 0.7rem;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--text-muted);
}

.legend-item {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

.legend-dot {
  width: 10px;
  height: 10px;
  border-radius: 2px;
}

/* Main */
.app-main {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
}

.card-header {
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: rgba(0,0,0,0.02);
}

.card-header h2 {
  margin: 0;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.ingredients-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  padding: 0;
  margin: 0;
  list-style: none;
}

.ingredients-grid li {
  padding: 0.6rem 1rem;
  border-bottom: 1px solid var(--border);
  border-right: 1px solid var(--border);
  font-size: 0.9rem;
}

.amount {
  color: var(--accent);
  font-weight: 700;
  margin-right: 0.4rem;
}

/* Stream View */
.stream-view {
  display: flex;
  flex-direction: column;
}

.step-group {
  display: flex;
  gap: 1rem;
  min-height: 80px;
}

.group-time {
  width: 70px;
  display: flex;
  flex-direction: column;
  align-items: center;
  flex-shrink: 0;
}

.time-label {
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--text-main);
  padding: 4px 8px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 6px;
  white-space: nowrap;
}

.time-line {
  flex-grow: 1;
  width: 2px;
  background: var(--border);
  margin: 4px 0;
}

.group-content {
  flex-grow: 1;
  padding-bottom: 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.group-content.parallel {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}

.step-card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1rem;
  border-left: 4px solid var(--accent);
}

.step-card.prep { border-left-color: var(--prep); }
.step-card.cook { border-left-color: var(--cook); }
.step-card.rest { border-left-color: var(--rest); }
.step-card.plating { border-left-color: var(--plating); }

.step-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}

.step-title {
  font-weight: 700;
  font-size: 1rem;
  line-height: 1.2;
}

.step-duration {
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--accent);
  background: rgba(128, 128, 128, 0.1);
  padding: 2px 8px;
  border-radius: 4px;
  white-space: nowrap;
}

.step-details {
  margin: 0 0 0.75rem 0;
  font-size: 0.9rem;
  color: var(--text-muted);
  line-height: 1.5;
}

.step-footer {
  display: flex;
  justify-content: flex-end;
}

.type-pill {
  font-size: 0.6rem;
  text-transform: uppercase;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 99px;
  background: rgba(0,0,0,0.05);
  color: var(--text-muted);
}

.finish-marker {
  display: flex;
  gap: 1rem;
  padding-bottom: 3rem;
}

.finish-label {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.finish-dot {
  width: 12px;
  height: 12px;
  background: var(--plating);
  border-radius: 50%;
  outline: 4px solid rgba(16, 185, 129, 0.2);
}

.finish-label span {
  font-weight: 700;
  font-size: 1rem;
  color: var(--plating);
}

.welcome-state {
  text-align: center;
  padding: 4rem 0;
  opacity: 0.5;
}

.welcome-state h2 {
  margin: 1rem 0 0.5rem;
}

.empty-icon {
  opacity: 0.3;
}

@media (max-width: 600px) {
  .container {
    padding: 0.75rem;
  }

  .app-header {
    gap: 1.5rem;
  }

  .header-right {
    justify-content: center;
  }

  .recipe-title {
    font-size: 1.4rem;
  }

  /* Stack time above content to maximize horizontal space */
  .step-group {
    flex-direction: column;
    gap: 0.5rem;
  }

  .group-time {
    width: 100%;
    flex-direction: row;
    align-items: center;
    gap: 0.75rem;
  }

  .time-label {
    font-size: 0.7rem;
    padding: 2px 6px;
  }

  .time-line {
    width: 100%;
    height: 1px;
    margin: 0;
  }

  .group-content {
    padding-left: 0.5rem;
    border-left: 2px solid var(--border);
    margin-left: 1.25rem; /* Align with middle of the time label */
    padding-bottom: 2rem;
  }

  .group-content.parallel {
    grid-template-columns: 1fr; /* Force stack parallel tasks on narrow screens */
  }

  .finish-marker {
    flex-direction: column;
    gap: 0.5rem;
  }

  .ingredients-grid {
    grid-template-columns: 1fr;
  }

  .mini-gantt-section {
    padding: 0.75rem;
  }
}
```
**index.html**

```html
<div id="root"></div>
```
**data.txt**

```txt
Same-day sourdough pizza (4 x ~12" pizzas)
Ingredients
Dough

600 g bread flour (or 520 g bread + 80 g 00)

390 g water (hold back ~30 g to adjust)

120 g active sourdough starter (100% hydration, bubbly and risen)

15 g fine salt

15 g olive oil (optional)

Semolina or extra flour for shaping

Sauce

1 x 800 g can whole tomatoes

8–12 g salt (to taste)

1 garlic clove, finely grated (optional)

1–2 tbsp olive oil

Pinch oregano (optional)

Cheese + toppings

400–500 g low-moisture mozzarella, shredded

Fresh mozzarella (optional), well-drained and torn small

Basil, pepperoni, mushrooms, olives, etc. (as desired)

Olive oil, chili flakes (optional)

Equipment

Pizza stone or steel (strongly preferred)

Pizza peel (or upside-down sheet pan)

Baking parchment (optional for easier launching)

Oven set to its highest practical temperature

Steps
1) Prepare the dough

Mix flour + most of the water.
In a large bowl, combine 600 g flour with ~360 g water (keep ~30 g back). Mix until no dry flour remains.

Rest 20 minutes.
Cover and let the dough sit.

Add starter + salt (+ oil).
Add 120 g starter and 15 g salt. Mix until incorporated. Add reserved water as needed to reach a soft, slightly tacky dough. If using olive oil, add it near the end and mix until absorbed.

Bulk rise with folds.
Cover and let rise at room temperature until the dough looks smoother and slightly domed and has risen roughly 30–60% (often 3–4 hours depending on warmth and starter strength).
During the first 90 minutes, do 3 folds about 30 minutes apart: lift and fold the dough over itself from each side 4 times, then cover again.

2) Divide and proof

Divide and ball.
Turn dough onto a lightly floured surface. Divide into 4 equal pieces (about 280 g each). Shape each into a tight ball by tucking edges underneath and dragging lightly to create surface tension.

Final proof.
Place balls into lightly oiled, covered containers. Proof at room temperature until relaxed and puffy (often 2–3 hours). If they start to overproof early, refrigerate to slow them down.

3) Make the sauce

Crush and season.
Hand-crush the tomatoes (or pulse briefly). Add salt, olive oil, and optional garlic/oregano. Set aside. Keep it simple; it should be loose but not watery.

4) Prep cheese and toppings

Drain fresh mozzarella (if using).
Slice/torn pieces, press between paper towels for 30–60 minutes to reduce water.

Prep toppings.
Slice mushrooms thin. If using watery vegetables, cook them briefly first or salt and drain to avoid soggy pizza.

Cheese ready.
Shred low-moisture mozzarella; keep toppings organized for fast assembly.

5) Heat the oven

Preheat thoroughly.
Put stone/steel on the upper-middle rack. Heat oven to maximum temperature for at least 45–60 minutes (longer is better) so the stone/steel is fully saturated.

6) Shape, top, and bake

Set up a station.
Flour/semolina for dusting, peel, sauce, cheese, toppings, and a cutting board.

Shape a dough ball.
Dust the ball and surface lightly. Press from the center outward, leaving a thicker rim. Stretch into ~12" round. Avoid rolling pins (they crush gas).

Load and top quickly.
Move dough to a dusted peel (or parchment). Add a thin layer of sauce, then cheese, then toppings. Less is more.

Bake.
Launch onto the hot stone/steel. Bake until the crust is well browned and cheese is bubbling:

Very hot ovens: ~6–10 minutes

Cooler ovens: ~10–14 minutes
If your oven has a strong broiler, finish the last 30–90 seconds under broiler for better top browning (watch closely).

Finish and serve.
Add basil after baking. Rest 1–2 minutes, slice, and serve. Repeat with remaining dough balls.
```
**infer.md**

```md
# Recipe Gantt & Ingredient Parser

Analyze the recipe in data.txt to extract a highly granular, parallelized cooking timeline. 

**CRITICAL RULE**: Do not summarize or skip steps. Every sub-bullet or distinct physical action in the recipe must be represented as a discrete `RecipeStep`. 

## Extraction Logic:
1. **Ingredients**: Parse every ingredient with its quantity/unit.
2. **Timeline Decomposition**:
   - Split instructions into the smallest possible logical tasks. 
   - **NO CLUMPING**: If a recipe step has sub-steps (e.g., "1a, 1b, 1c"), create three separate `RecipeStep` objects.
   - **FULL TEXT**: Extract the full descriptive text from the source into the `details` field. Include all specific tips, temperatures, and sensory cues (e.g., "mix until no dry flour remains", "until slightly domed").
   - Assign a `type`: 
     - `prep`: Initial setup, chopping, measuring.
     - `cook`: Active heat application (frying, boiling).
     - `rest`: Passive phases (dough rising, meat resting, oven preheating).
     - `plating`: Final assembly.
3. **Parallel Execution**: 
   - Parallelize tasks where physically possible. 
   - While something is "resting" or "baking" or "preheating" (passive), the user should be performing the next "prep" or "active" tasks.
   - For recipes that repeat (like 4 pizzas), represent the first cycle in detail.
4. **Duration Estimates**: Use the specific times mentioned. If a range is given (6-10 mins), use the midpoint or the safer upper bound.

## Data Constraints:
- `startTime` and `duration` are in minutes.
- `totalTimeMinutes` is the timestamp when the final task finishes.
- Ensure the `task` strings are concise (max 40 chars) to prevent UI overflow.

## Specific Task for Pizza Example:
If parsing the provided Sourdough Pizza recipe, ensure the 4-hour bulk rise and 1-hour oven preheat are treated as parallel opportunities for sauce prep and topping prep.
```
**insight.json**

```json
{
  "recipeTitle": "Same-Day Sourdough Pizza",
  "ingredients": [
    { "item": "Bread Flour", "amount": "600g" },
    { "item": "Water", "amount": "390g" },
    { "item": "Sourdough Starter", "amount": "120g" },
    { "item": "Fine Salt", "amount": "15g" },
    { "item": "Olive Oil", "amount": "15g" },
    { "item": "Whole Tomatoes", "amount": "800g can" },
    { "item": "Low-moisture Mozzarella", "amount": "400-500g" },
    { "item": "Fresh Basil", "amount": "to taste" }
  ],
  "totalTimeMinutes": 420,
  "steps": [
    { "id": "1", "task": "Mix flour and water", "details": "Combine 600g flour with 360g water in a large bowl. Mix until no dry flour remains.", "startTime": 0, "duration": 10, "type": "prep" },
    { "id": "2", "task": "Autolyse rest", "details": "Cover and let dough sit for 20 minutes to hydrate.", "startTime": 10, "duration": 20, "type": "rest" },
    { "id": "3", "task": "Add starter and salt", "details": "Add 120g starter and 15g salt. Mix until incorporated. Add remaining water as needed for soft, tacky dough.", "startTime": 30, "duration": 10, "type": "prep" },
    { "id": "4", "task": "Bulk rise with folds", "details": "Cover and rise at room temp. Do 3 stretch-and-folds every 30 mins during first 90 mins. Total rise until 30-60% bigger.", "startTime": 40, "duration": 180, "type": "rest" },
    { "id": "5", "task": "Make pizza sauce", "details": "Hand-crush tomatoes. Mix in salt, olive oil, optional garlic and oregano. Keep loose but not watery.", "startTime": 100, "duration": 10, "type": "prep" },
    { "id": "6", "task": "Prep and drain mozzarella", "details": "If using fresh mozz, slice and press between paper towels for 30-60 mins to reduce moisture.", "startTime": 110, "duration": 5, "type": "prep" },
    { "id": "7", "task": "Prep toppings", "details": "Slice mushrooms thin. Pre-cook watery vegetables or salt and drain. Shred low-moisture mozzarella.", "startTime": 115, "duration": 15, "type": "prep" },
    { "id": "8", "task": "Divide and ball dough", "details": "Turn onto floured surface. Divide into 4 pieces (~280g each). Shape into tight balls with surface tension.", "startTime": 220, "duration": 15, "type": "prep" },
    { "id": "9", "task": "Final proof", "details": "Place balls in oiled containers. Proof at room temp until relaxed and puffy (2-3 hours).", "startTime": 235, "duration": 120, "type": "rest" },
    { "id": "10", "task": "Preheat oven and stone", "details": "Place stone on upper-middle rack. Heat to max temp for 45-60 mins until stone is fully saturated with heat.", "startTime": 295, "duration": 60, "type": "rest" },
    { "id": "11", "task": "Set up assembly station", "details": "Arrange flour/semolina, peel, sauce, cheese, toppings, and cutting board for fast assembly.", "startTime": 355, "duration": 5, "type": "prep" },
    { "id": "12", "task": "Shape first pizza", "details": "Dust ball and surface. Press from center outward, leaving thicker rim. Stretch to ~12 inch round. No rolling pin.", "startTime": 360, "duration": 5, "type": "prep" },
    { "id": "13", "task": "Top pizza", "details": "Move to dusted peel. Add thin sauce layer, then cheese, then toppings. Less is more.", "startTime": 365, "duration": 3, "type": "prep" },
    { "id": "14", "task": "Bake pizza", "details": "Launch onto hot stone. Bake 6-10 mins until crust browned and cheese bubbling. Optional broiler finish for 30-90 secs.", "startTime": 368, "duration": 10, "type": "cook" },
    { "id": "15", "task": "Rest, slice, and serve", "details": "Add fresh basil. Rest 1-2 mins, slice, serve. Repeat shaping and baking for remaining 3 pizzas.", "startTime": 378, "duration": 42, "type": "plating" }
  ]
}

```
**config.json**

```json
{
  "dependencies": {
    "react": "^19.2.3",
    "react-dom": "^19.2.3",
    "lucide-react": "^0.563.0"
  },
   "description": "Let Dr. Sous turn any recipe into a cooking timeline.",
   "inference": {
    "title": "Dr. Sous Recipe Planner",
    "dataTitle": "Raw Recipe Text",
    "submitTitle": "Generate Recipe Plan"   
  }
}
```