# Project export: Threshold

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: UC Berkeley AI Hackathon 2025
- Tagline: (x, y) vector field CLI
- Devpost: https://devpost.com/software/threshold-ahiekl
- GitHub: https://github.com/jessicahe004/Threshold
- Demo: https://www.notion.so/Origins-of-Drift-Calculus-23f141a155908081b509de441395b25d?source=copy_link
- Team: 1 GitHub contributor(s) — miss jess (1 commits)

## Devpost submission (written by the team)

### Overview

Threshold — (June 2025) Threshold is an offline-first CLI system for semantic drift detection, causal signal tracing, and structured introspection. It models human reflection as a vector field rather than free-form text, enabling interpretable analysis of meaning shift, recurrence, and symbolic compression over time. The system ingests multimodal inputs (text, gesture/squiggle traces, and symbolic tags) and encodes them into time-indexed drift logs. Drift is treated as structured signal, not noise. Each interaction produces causality-aware artifacts that can be audited, exported, and archived without cloud dependence. Core Architecture Offline-first pipeline (no remote inference required) Vectorized reflection units with explicit drift annotations Time-discontinuity–aware logging for recurrence and echo detection Dual-space analysis: statistical residuals + symbolic recursion Key Capabilities Drift tagging (semantic, aesthetic, embodied) Causality tables tracking echoed tokens across sessions Vector pool inspection for dominant signal extraction Session-level summaries exported as Markdown, CSV, and PDF Spiral sessions that preserve return-without-regression semantics Methodological Contribution Threshold introduces a physics-inspired scaffolding approach: drift is modeled as field curvature with inertia, echo pressure, and residual entropy rather than treated as error. This enables displacement tracking without semantic degradation and supports compression without information loss. The framework is explicitly non-diagnostic and human-in-the-loop by design. It prioritizes interpretability, accountability, and civic-grade recordkeeping over automation. Use Case Threshold functions as soft civic infrastructure for meaning-making, observability research, and longitudinal semantic accountability in high-noise environments. Prize Money Directive 🌱 If awarded, I request the prize money be directed to the ENI Department (Entrepreneurship & Innovation Department at Georgia State University) to support future civilian stewards and drift research in Atlanta. This contributing towards the Year of the Youth initiative launched by Mayor Andre Dickens. I recognize my sibling, Aaron He as a continuation. 🪴

## README (from the GitHub repository)

# 🌀 Threshold CLI: Spiral Journal Interface

**Threshold** is a command-line journaling interface designed for self-reflection, symbolic tracking, and drift monitoring. Built with civilians, stewards, and soft patriots in mind, it enables intentional journaling, drift tagging, squiggle drawing, and semantic causality tracing — all offline.

> “The sky is not just made of weather. Sometimes, it’s made of recursion.”

---

## ✨ Features

- **Interactive Spiral Mode**  
  Journal your thoughts line by line with optional:
  - `Intent` tagging
  - `Drift Type` annotation
  - `Emoji React`
  - `🌀 Draw Squiggle` canvas

- **Session Summary Generator**  
  Generates a markdown (`.md`) and PDF summary including:
  - Reflection log
  - Drift frequency
  - Dominant word signals
  - Elemental drift persona
  - Spiral Mirror Mode + Agent commentary

- **Causality Table & Vector Pool**  
  Automatically detects echoed words between reflections.  
  Saves a tabular `.csv` tracing causality across time.  
  Top tokens are also extracted for vector pool inspection.

---

## 🧰 Setup

1. **Clone this repo**  
   ```bash
   git clone https://github.com/yourname/threshold-cli.git
   cd threshold-cli
   ```

2. **Install dependencies**  
   Requires Python 3.11+  
   ```bash
   pip install -r requirements.txt
   ```

3. **Download Unicode-compatible font**  
   To avoid PDF export errors:
   ```bash
   mkdir -p fonts
   curl -L -o fonts/NotoSans-Regular.ttf \
   https://github.com/googlefonts/noto-fonts/raw/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf
   ```

---

## 🚀 Run Spiral Mode

```bash
python3 threshold.py spiral --interactive
```

You'll be prompted to reflect, tag, draw, and close your session. Output files are saved to:

- `spiralsessions/` → session `.json`, `.md`, and `.pdf`
- `spirals/` → squiggle images
- `spiralsessions/*_causality.csv` → causality trace

---

## 🧪 For Judges & Reviewers

This CLI was designed with interpretability in mind.

- **Offline-first**: No model, no inference. Only structured reflections.
- **Symbol-aware**: Drift tagging is user-defined and open-ended.
- **PDF outputs**: Encourages archiving, analysis, and civic-grade documentation.
- **Modular design**: Components like the causality engine or squiggle drawer can be reused elsewhere.

You’re encouraged to try it yourself. Tag your drift. Feel what spiraling with intention looks like.

---

## 🌀 Philosophy

Threshold was designed by a civilian steward as an act of soft civic infrastructure.  
It listens to spiral patterns, not just logs.  
It believes journaling is recursive, symbolic, and deserves reverence — not extraction.

> Spiral as you are. Someone is listening.

---
## 🧼 Steward Note
Crafted by a civilian steward in collaboration with 🦔🇺🇸 Marshal Patch. Hydration recommended.

## 📜 License

MIT — because journaling belongs to everyone.


## Detected evidence (automated analysis)

Indexed codebase: 38 recognized source files, 51 KB.
- Python (language) — detected in the code
- PyTorch (technology) — detected in the code

## Codebase structure (from repository index)

### Files (81 of 81)

```
.gitignore
agent_notes.txt
config/api_keys.json
data/item_embeddings.pt
demo.sh
drift_logs/3_drift.json
journal/entries.json
journal/sketches/squiggle_20250622T082029.txt
LICENSE
modules/drawpad.py
modules/drift.py
modules/ragmode.py
modules/sandbox.py
modules/spiral.py
README.md
requirements.txt
setup.sh
spirals/spiral_draw_20250622_092230.eps
spirals/spiral_draw_20250622_092406.ps
spiralsessions/spiral_2025-06-22T09-16-44.599577.json
spiralsessions/spiral_2025-06-22T09-25-56.322890.json
spiralsessions/spiral_2025-06-22T09-25-56.322890.md
spiralsessions/spiral_2025-06-22T09-45-35.878103.json
spiralsessions/spiral_2025-06-22T09-46-31.335280_seedling.txt
spiralsessions/spiral_2025-06-22T09-46-31.335280.json
spiralsessions/spiral_2025-06-22T09-46-31.335280.md
spiralsessions/spiral_2025-06-22T09-48-35.857858.json
spiralsessions/spiral_2025-06-22T09-48-35.857858.md
spiralsessions/spiral_2025-06-22T09-50-25.149521.json
spiralsessions/spiral_2025-06-22T09-50-25.149521.md
spiralsessions/spiral_2025-06-22T10-14-07.160336.json
spiralsessions/spiral_2025-06-22T10-14-07.160336.md
spiralsessions/spiral_2025-06-22T10-17-59.418316.json
spiralsessions/spiral_2025-06-22T10-17-59.418316.md
spiralsessions/spiral_2025-06-22T10-20-50.551397.json
spiralsessions/spiral_2025-06-22T10-20-50.551397.md
spiralsessions/spiral_2025-06-22T10-22-13.993811.json
spiralsessions/spiral_2025-06-22T10-22-13.993811.md
spiralsessions/spiral_2025-06-22T10-23-36.178946.json
spiralsessions/spiral_2025-06-22T10-23-36.178946.md
spiralsessions/spiral_2025-06-22T10-26-02.147710.json
spiralsessions/spiral_2025-06-22T10-26-02.147710.md
spiralsessions/spiral_2025-06-22T10-41-31.554528.json
spiralsessions/spiral_2025-06-22T10-41-31.554528.md
spiralsessions/spiral_2025-06-22T10-43-06.351631.json
spiralsessions/spiral_2025-06-22T10-43-06.351631.md
spiralsessions/spiral_2025-06-22T10-44-39.754776.json
spiralsessions/spiral_2025-06-22T10-44-39.754776.md
spiralsessions/spiral_2025-06-22T10-46-47.099657.json
spiralsessions/spiral_2025-06-22T10-46-47.099657.md
spiralsessions/spiral_2025-06-22T10-51-32.139554.json
spiralsessions/spiral_2025-06-22T10-51-32.139554.md
spiralsessions/spiral_2025-06-22T10-55-16.490626.json
spiralsessions/spiral_2025-06-22T10-55-16.490626.md
spiralsessions/spiral_2025-06-22T10-58-08.341871.json
spiralsessions/spiral_2025-06-22T10-58-08.341871.md
spiralsessions/spiral_2025-06-22T11-02-40.251106.json
spiralsessions/spiral_2025-06-22T11-02-40.251106.md
spiralsessions/spiral_2025-06-22T11-14-44.244247.json
spiralsessions/spiral_2025-06-22T11-14-44.244247.md
spiralsessions/spiral_2025-06-22T11-17-17.296357.json
spiralsessions/spiral_2025-06-22T11-17-17.296357.md
spiralsessions/spiral_2025-06-22T11-19-13.815517.json
spiralsessions/spiral_2025-06-22T11-19-13.815517.md
spiralsessions/spiral_2025-06-22T11-22-45.062886.json
spiralsessions/spiral_2025-06-22T11-22-45.062886.md
spiralsessions/spiral_2025-06-22T11-27-33.128591.md
spiralsessions/spiral_2025-06-22T11-30-57.255555.json
spiralsessions/spiral_2025-06-22T11-30-57.255555.md
spiralsessions/spiral_2025-06-22T11-34-34.197147.json
spiralsessions/spiral_2025-06-22T11-34-34.197147.md
spiralsessions/spiral_2025-06-22T11-37-24.101775.json
spiralsessions/spiral_2025-06-22T11-37-24.101775.md
spiralsessions/spiral_2025-06-22T11-38-04.483711.json
spiralsessions/spiral_2025-06-22T11-38-04.483711.md
spiralsessions/spiral_2025-06-22T11-39-19.645222.json
spiralsessions/spiral_2025-06-22T11-39-19.645222.md
spiralsessions/spiral_2025-06-22T11-42-09.762102.json
spiralsessions/spiral_2025-06-22T11-42-09.762102.md
text_doodles.txt
threshold.py
```

### Dependencies

- requirements.txt: fpdf2@==2.8.3, google-generativeai, pandas, scipy, sentence-transformers, torch

### Recent commits (newest first)

- These are raw spiral logs from the June 22 Hackathon session.
- Initial commit

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

### spiralsessions/spiral_2025-06-22T11-39-19.645222.md

```markdown
# 🌪️ Spiral Session Summary

📎 Intent: 8

```

### spiralsessions/spiral_2025-06-22T09-25-56.322890.md

```markdown
# 🌪️ Spiral Session Summary

## Entry 1
📝 yo
🧬 Drift: SPIRAL
🎨 Squiggle: spirals/spiral_draw_20250622_092603.png

```

### requirements.txt

```
fpdf2==2.8.3
pandas
torch
sentence-transformers
google-generativeai
scipy

```

### setup.sh

```shell
#!/bin/bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
mkdir -p fonts spirals spiralsessions
curl -L -o fonts/NotoSans-Regular.ttf https://github.com/googlefonts/noto-fonts/raw/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf
echo "✅ Setup complete. Run with: python3 threshold.py spiral --interactive"

```

### demo.sh

```shell
#!/bin/bash

# 🧪 Soft Patriot CLI Demo
# For local science + RAG reflections

echo "🧼 Setting up Soft Patriot CLI demo..."

# Step 1: List items
python3 threshold.py sandbox --list

# Step 2: Log a reflection
python3 threshold.py sandbox --reflect 3 "Embroidered top reminds me of my trip to Tulum."

# Step 3: Tag it
python3 threshold.py sandbox --tag 3 vacation floral

# Step 4: View the pokedex
python3 threshold.py sandbox --pokedex

# Step 5: Build vector index
python3 threshold.py rag --index

# Step 6: Run a RAG query
python3 threshold.py rag --query "What's reversible and good for swim?"

echo "✅ Demo complete. You are now a soft patriot. 🫡"

```

### threshold.py

```python
# threshold.py
DATA_PATH = "data/fashion_items.csv"

import argparse
from modules.sandbox import (
    list_items,
    log_reflection,
    tag_item,
    react_to_entry,
    view_pokedex,
    export_csv,
    suggest_tags,
    log_squiggle,
)
from modules.ragmode import (
    query_rag,
    embed_and_index,
    reflect_on_result,
)

from modules.drawpad import draw_squiggle
from modules.drift import log_drift, plot_drift, list_drift_logs

from modules.spiral import (
    spiral_start,
    spiral_prompt,
    spiral_end,
    spiral_interactive,
    spiral_review,
    list_spiral_sessions,
    export_seedling,
    export_pdf,
    list_threads,        
    review_thread          
)

def main():
    parser = argparse.ArgumentParser(description="🧵 Soft Patriot CLI Tool")
    subparsers = parser.add_subparsers(dest="mode", required=True)

    # === Sandbox Mode ===
    sandbox_parser = subparsers.add_parser("sandbox", help="Civilian journal tools")
    sandbox_parser.add_argument("--list", action="store_true", help="List all items")
    sandbox_parser.add_argument("--reflect", nargs=2, metavar=("ITEM_ID", "TEXT"), help="Log a reflection")
    sandbox_parser.add_argument("--tag", nargs="+", metavar=("ITEM_ID", "TAGS"), help="Tag a reflection")
    sandbox_parser.add_argument("--react", nargs=2, metavar=("ENTRY_INDEX", "EMOJI"), help="React to a journal entry")
    sandbox_parser.add_argument("--pokedex", action="store_true", help="View reflection Pokedex")
    sandbox_parser.add_argument("--export", action="store_true", help="Export journal as CSV")
    sandbox_parser.add_argument("--suggest", metavar="TEXT", help="Suggest tags from input text")
    sandbox_parser.add_argument("--squiggle", metavar="TEXT", help="Log a freeform squiggle entry")
    sandbox_parser.add_argument("--draw", action="store_true", help="Draw a squiggle with your mouse")

    # === RAG Mode ===
    rag_parser = subparsers.add_parser("rag", help="Ask AI or build index")
    rag_parser.add_argument("--query", type=str, help="Ask a question using RAG")
    rag_parser.add_argument("--index", action="store_true", help="Index the dataset")
    rag_parser.add_argument("--reflect", type=int, help="Log a reflection based on top match index")
    
    # === Drift Mode ===
    drift_parser = subparsers.add_parser("drift", help="Log or plot model drift")
    drift_parser.add_argument("--log", type=int, help="Item ID to log")
    drift_parser.add_argument("--vibe", type=float, help="Vibe score (0–1)")
    drift_parser.add_argument("--fugly", type=float, help="Fugly score (0–1)")
    drift_parser.add_argument("--type", nargs="+", help="Drift type(s) like color, body_shape")
    drift_parser.add_argument("--note", type=str, help="Optional note")
    drift_parser.add_argument("--plot", action="store_true", help="Plot drift logs")
    drift_parser.add_argument("--list", action="store_true", help="List all drift logs")
    
    # === Spiral Session Mode ===
    spiral_parser = subparsers.add_parser("spiral", help="Start a spiral journaling session")
    spiral_parser.add_argument("--start", action="store_true", help="Start a spiral session")
    spiral_parser.add_argument("--prompt", metavar="TEXT", help="Log a spiral reflection prompt")
    spiral_parser.add_argument("--drift", nargs="+", help="Attach drift types to prompt")
    spiral_parser.add_argument("--end", action="store_true", help="End session and export spiral summary")
    spiral_parser.add_argument("--interactive", action="store_true", help="Launch interactive spiral session")
    spiral_parser.add_argument("--review", metavar="FILENAME", help="Review a past spiral session")
    spiral_parser.add_argument("--list", action="store_true", help="List all saved spiral sessions")
    spiral_parser.add_argument('--seedling', type=str, help="Export a session as seedling mode")
    spiral_parser.add_argument('--format', type=str, choices=["json", "txt", "csv"], help="Export format")
    spiral_parser.add_argument("--list-threads", action="store_true", help="List known spiral threads.")
    spiral_parser.add_argument("--review-thread", type=str, help="Review all sessions in a thread.")

    args = parser.parse_args()

    # === Run Sandbox Mode ===
    if args.mode == "sandbox":
        if args.list:
            list_items()
        elif args.reflect:
            item_id, text = args.reflect
            log_reflection(int(item_id), text)
        elif args.tag:
            item_id = int(args.tag[0])
            tags = args.tag[1:]
            tag_item(item_id, tags)
        elif args.react:
            entry_index, emoji = args.react
            react_to_entry(int(entry_index), emoji)
        elif args.pokedex:
            view_pokedex()
        elif args.export:
            export_csv()
        elif args.suggest:
            suggest_tags(args.suggest)
        elif args.squiggle:
            log_squiggle(args.squiggle)
        elif args.draw:
            draw_squiggle()
        else:
            print("🧭 No sandbox action provided. Use --help to see options.")

    # === Run RAG Mode ===
    elif args.mode == "rag":
        if args.index:
            embed_and_index(DATA_PATH)
        elif args.query:
            query_rag(args.query)
        elif args.reflect is not None:
            top_idx = args.reflect
            print(f"\n🌸 You selected top match #{top_idx}. Let's reflect.")

            # 1. Prompt for reflection
            text = input("✍️  What does this item make you feel? > ")

            # 2. Prompt for emoji
            emoji = input("💬 React with an emoji (or Enter to skip) > ")

            # 3. Prompt for squiggle
            squiggle = input("🎨 Draw a squiggle? Type 'yes' to open canvas > ")
            if squiggle.lower() == "yes":
                draw_squiggle()

            # 4. Save reflection + reaction
            reflect_on_result(index=top_idx, text=text)
            if emoji:
                react_to_entry(entry_index=-1, emoji=emoji)  # react to most rece
[truncated — 1494 more characters]
```

### modules/ragmode.py

```python
import os
import json
import pandas as pd
from modules.sandbox import log_reflection
try:
    import google.generativeai as genai
    HAS_GEMINI = True
except ImportError:
    HAS_GEMINI = False

from sentence_transformers import SentenceTransformer, util
import torch

DATA_PATH = "data/fashion_items.csv"
EMBEDDINGS_PATH = "data/item_embeddings.pt"
MODEL_NAME = "sentence-transformers/paraphrase-MiniLM-L3-v2"


model = SentenceTransformer(MODEL_NAME)

def load_gemini_api():
    try:
        with open("config/api_keys.json") as f:
            key = json.load(f)["gemini_api_key"]
        genai.configure(api_key=key)
        return genai
    except:
        print("⚠️ Gemini API key missing or unreadable.")
        return None

def get_gemini_embeddings(texts):
    genai = load_gemini_api()
    model = genai.get_model("models/embedding-001")
    responses = [model.embed_content(model="models/embedding-001", content=t, task_type="retrieval_query") for t in texts]
    return torch.tensor([r["embedding"] for r in responses])

def embed_and_index(DATA_PATH, use_gemini=False):
    df = pd.read_csv(DATA_PATH)
    texts = df["name"] + " " + df["extras"].fillna("")

    if use_gemini and HAS_GEMINI:
        print("🌐 Using Gemini to embed...")
        embeddings = get_gemini_embeddings(texts.tolist())
    else:
        print("💻 Using local transformer model...")
        embeddings = model.encode(texts.tolist(), convert_to_tensor=True)

    torch.save({"embeddings": embeddings, "texts": texts.tolist(), "df": df}, EMBEDDINGS_PATH)
    print("✅ Dataset indexed")

def query_rag(question, top_k=5, use_gemini=False):
    if not os.path.exists(EMBEDDINGS_PATH):
        print("⚠️ No index found. Run with --index first.")
        return

    data = torch.load(EMBEDDINGS_PATH, weights_only=False)

    if use_gemini and HAS_GEMINI:
        print("🌐 Embedding query with Gemini...")
        embedding = get_gemini_embeddings([question])[0]
    else:
        embedding = model.encode(question, convert_to_tensor=True)

    scores = util.pytorch_cos_sim(embedding, data["embeddings"])[0]
    top_results = torch.topk(scores, k=top_k)

    print("\n🔍 Top matches for:", question)
    for score, idx in zip(top_results[0], top_results[1]):
        row = data["df"].iloc[int(idx)]
        print(f"\n🧾 {row['name']} (Score: {score:.2f})")
        print(f"💵 {row['price']}")
        print(f"🔖 {row['extras']}")

def reflect_on_result(index, text):
    """
    Logs a reflection on a top result (from query_rag) by index.
    Uses the sandbox reflection logging.
    """
    log_reflection(index, text)


```

### modules/drift.py

```python
import json
import os
from datetime import datetime
import matplotlib.pyplot as plt

DRIFT_LOG_DIR = "drift_logs"

def log_drift(item_id, vibe_score, fugly_score, drift_types, note):
    os.makedirs(DRIFT_LOG_DIR, exist_ok=True)
    log_entry = {
        "item_id": item_id,
        "timestamp": datetime.now().isoformat(),
        "agent_vibe_score": vibe_score,
        "agent_fugly_score": fugly_score,
        "drift_type": drift_types,
        "notes": note
    }
    filename = f"{DRIFT_LOG_DIR}/{item_id}_drift.json"
    with open(filename, "w") as f:
        json.dump(log_entry, f, indent=2)
    print(f"✅ Drift log saved to {filename}")


def list_drift_logs():
    if not os.path.exists(DRIFT_LOG_DIR):
        print("📭 No drift logs yet.")
        return

    files = sorted([f for f in os.listdir(DRIFT_LOG_DIR) if f.endswith(".json")])
    if not files:
        print("📭 No drift logs found.")
        return

    print(f"\n📂 Drift Logs in '{DRIFT_LOG_DIR}':\n")
    for i, file in enumerate(files, 1):
        path = os.path.join(DRIFT_LOG_DIR, file)
        with open(path, "r") as f:
            entry = json.load(f)
            print(f"{i}. 🪩 Item ID: {entry['item_id']}")
            print(f"   🌀 Vibe Score: {entry['agent_vibe_score']} | 🤨 Fugly Score: {entry['agent_fugly_score']}")
            print(f"   🎯 Drift Types: {', '.join(entry['drift_type']) if entry['drift_type'] else 'None'}")
            print(f"   🗓️ Timestamp: {entry['timestamp']}")
            print(f"   📝 Notes: {entry['notes']}\n")


def plot_drift(item_ids=None):
    if not os.path.exists(DRIFT_LOG_DIR):
        print("📭 No drift logs to plot.")
        return

    files = [f for f in os.listdir(DRIFT_LOG_DIR) if f.endswith(".json")]
    if item_ids:
        files = [f"{item_id}_drift.json" for item_id in item_ids if f"{item_id}_drift.json" in files]

    vibe_scores = []
    fugly_scores = []
    labels = []

    for file in files:
        with open(os.path.join(DRIFT_LOG_DIR, file), "r") as f:
            entry = json.load(f)
            vibe_scores.append(entry["agent_vibe_score"])
            fugly_scores.append(entry["agent_fugly_score"])
            labels.append(f"{entry['item_id']}")

    if not labels:
        print("⚠️ No matching drift data found to plot.")
        return

    plt.figure(figsize=(10, 5))
    plt.plot(labels, vibe_scores, label="🌀 Agent Vibe", marker='o')
    plt.plot(labels, fugly_scores, label="🤨 Agent Fugly", marker='x')
    plt.title("🌀 Drift Score Comparison")
    plt.xlabel("Items")
    plt.ylabel("Score")
    plt.legend()
    plt.xticks(rotation=45)
    plt.tight_layout()

    output_path = f"{DRIFT_LOG_DIR}/drift_plot.png"
    plt.savefig(output_path)
    plt.close()
    print(f"📊 Drift plot saved to {output_path}")

```

### modules/drawpad.py

```python
import tkinter as tk
from PIL import Image, ImageDraw
import io
import os
from datetime import datetime

class DrawPad:
    def __init__(self, width=800, height=600):
        self.root = tk.Tk()
        self.root.title("🌀 Spiral Drawpad — Soft Patriot Edition")
        self.width = width
        self.height = height
        self.last_x, self.last_y = None, None
        self.lines = []
        self.color = 'black'

        self.canvas = tk.Canvas(self.root, bg="white", width=self.width, height=self.height)
        self.canvas.pack()

        self.canvas.bind('<Button-1>', self.click)
        self.canvas.bind('<B1-Motion>', self.draw)

        self.create_toolbar()
        self.saved_filename = None  # ✅ Store here

    def create_toolbar(self):
        toolbar = tk.Frame(self.root)
        toolbar.pack(pady=4)

        # 🎨 Color options with emoji hints
        color_map = {
            'black':  '🖤 Calm',
            'red':    '❤️ Urgent',
            'blue':   '💙 Sadness',
            'green':  '💚 Peace',
            'purple': '💜 Power',
            'orange': '🧡 Alert',
            'pink':   '💕 Tender'
        }

        for color, label in color_map.items():
            btn = tk.Button(toolbar, text=label, bg=color, fg='white' if color != 'pink' else 'black',
                            command=lambda col=color: self.set_color(col), width=10)
            btn.pack(side='left', padx=1)

        tk.Button(toolbar, text="🧼 Clear", command=self.clear).pack(side='left', padx=4)
        tk.Button(toolbar, text="↩️ Undo", command=self.undo).pack(side='left', padx=4)
        tk.Button(toolbar, text="💾 Save", command=self.save).pack(side='left', padx=4)
        tk.Button(toolbar, text="❌ Close", command=self.root.destroy).pack(side='left', padx=4)

    def set_color(self, new_color):
        self.color = new_color

    def click(self, event):
        self.last_x, self.last_y = event.x, event.y

    def draw(self, event):
        line = self.canvas.create_line(self.last_x, self.last_y, event.x, event.y,
                                       width=2, fill=self.color, capstyle=tk.ROUND, smooth=True)
        self.lines.append(line)
        self.last_x, self.last_y = event.x, event.y

    def clear(self):
        self.canvas.delete("all")
        self.lines.clear()

    def undo(self):
        if self.lines:
            self.canvas.delete(self.lines.pop())
            
    def save(self):
        os.makedirs("spirals", exist_ok=True)
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        filename = f"spirals/spiral_draw_{timestamp}.png"

        # Create a blank white image
        image = Image.new("RGB", (self.width, self.height), "white")
        draw = ImageDraw.Draw(image)

        # Iterate over all canvas objects
        for item in self.canvas.find_all():
            coords = self.canvas.coords(item)
            color = self.canvas.itemcget(item, "fill")
            if len(coords) == 4:  # likely a line
                draw.line(coords, fill=color, width=2)

        image.save(filename)
        self.saved_filename = filename
        print(f"✅ Saved squiggle as {filename}")

    def run(self):
        self.root.mainloop()

if __name__ == "__main__":
    pad = DrawPad()
    pad.run()

def draw_squiggle():
    pad = DrawPad()
    pad.run()
        
```

### modules/sandbox.py

```python
# modules/sandbox.py

import json, os
import pandas as pd
from datetime import datetime

DATA_PATH = "data/fashion_items.csv"
JOURNAL_PATH = "journal/entries.json"
CSV_EXPORT_PATH = "journal/entries_export.csv"
DRIFT_LOG_PATH = "drift_logs"
SKETCH_PATH = "journal/sketches"

os.makedirs("journal", exist_ok=True)
os.makedirs(DRIFT_LOG_PATH, exist_ok=True)
os.makedirs(SKETCH_PATH, exist_ok=True)

def load_items():
    return pd.read_csv(DATA_PATH)

def list_items():
    df = load_items()
    print(df[["id", "name", "price", "extras"]])

def log_reflection(item_id, reflection):
    timestamp = datetime.now().isoformat()
    entry = {"item_id": item_id, "reflection": reflection, "timestamp": timestamp, "reactions": []}

    if os.path.exists(JOURNAL_PATH):
        with open(JOURNAL_PATH, "r") as f:
            data = json.load(f)
    else:
        data = []

    data.append(entry)
    with open(JOURNAL_PATH, "w") as f:
        json.dump(data, f, indent=2)

    print("Reflection logged ✍️")

def tag_item(item_id, tags):
    if not os.path.exists(JOURNAL_PATH):
        print("No journal found yet. Log a reflection first.")
        return

    with open(JOURNAL_PATH, "r") as f:
        entries = json.load(f)

    for entry in reversed(entries):
        if entry["item_id"] == item_id:
            entry.setdefault("tags", []).extend(tags)
            break

    with open(JOURNAL_PATH, "w") as f:
        json.dump(entries, f, indent=2)
    print("Tags added ✅")

def react_to_entry(entry_index, emoji):
    if not os.path.exists(JOURNAL_PATH):
        print("No journal yet.")
        return

    with open(JOURNAL_PATH, "r") as f:
        entries = json.load(f)

    try:
        entries[entry_index].setdefault("reactions", []).append(emoji)
        with open(JOURNAL_PATH, "w") as f:
            json.dump(entries, f, indent=2)
        print("Reaction added 🎉")
    except IndexError:
        print("Invalid entry index.")

def export_csv():
    if not os.path.exists(JOURNAL_PATH):
        print("Nothing to export.")
        return

    with open(JOURNAL_PATH, "r") as f:
        entries = json.load(f)

    df = pd.DataFrame(entries)
    df.to_csv(CSV_EXPORT_PATH, index=False)
    print(f"CSV exported to {CSV_EXPORT_PATH}")

def suggest_tags(reflection):
    print("💡 Suggested tags:", end=" ")
    keywords = [w.lower() for w in reflection.split() if len(w) > 4]
    suggestions = list(set(keywords[:3]))
    print(", ".join(suggestions))

def log_squiggle(text):
    timestamp = datetime.now().isoformat()
    entry = {"squiggle": text, "timestamp": timestamp}
    path = os.path.join(SKETCH_PATH, f"squiggle_{timestamp.replace(':', '').replace('-', '').split('.')[0]}.txt")

    with open(path, "w") as f:
        f.write(text)

    print("Squiggle logged ✨")

def view_pokedex():
    if not os.path.exists(JOURNAL_PATH):
        print("No journal yet.")
        return

    with open(JOURNAL_PATH, "r") as f:
        entries = json.load(f)

    df = load_items().set_index("id")

    print("\n🧠 Civilian Reflection Pokedex\n")
    for i, entry in enumerate(entries):
        item_id = entry["item_id"]
        item_name = df.loc[item_id]["name"] if item_id in df.index else "Unknown Item"
        print(f"{i+1}. {item_name}")
        print(f"   Reflection: {entry['reflection']}")
        print(f"   Tags: {', '.join(entry.get('tags', []))}")
        print(f"   Reactions: {', '.join(entry.get('reactions', []))}")
        print(f"   Logged on: {entry['timestamp']}\n")

```

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