From 9c51c582284cabf0beace9855753f8fc2afe2a7a Mon Sep 17 00:00:00 2001 From: jbrestarted Date: Sat, 31 Jul 2021 15:21:31 -0600 Subject: [PATCH 1/4] updating our h1 tag with our name --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index a169a449c..4784ccc40 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,7 @@

Hi I'm

-

Your Name Here

+

Jeremy Bruso

Introduction

From f414f4d28c12da24e77adc91f914d71554309f18 Mon Sep 17 00:00:00 2001 From: jbrestarted Date: Sat, 31 Jul 2021 17:22:04 -0600 Subject: [PATCH 2/4] updating our details --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 4784ccc40..178f80984 100644 --- a/index.html +++ b/index.html @@ -17,9 +17,9 @@

Jeremy Bruso

Introduction

-

Details here...

+

I am a software developer

Where I'm From

-

Details here...

+

San Diego, CA

What are your favorite hobbies?

Details here...

What's your dream job?

From 2efe6d95e90e86ae9ca52528d4e999ee85e6fca2 Mon Sep 17 00:00:00 2001 From: jbrestarted Date: Sat, 31 Jul 2021 20:40:24 -0600 Subject: [PATCH 3/4] updating our details --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 178f80984..d94c0d827 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@

Where I'm From

What are your favorite hobbies?

Details here...

What's your dream job?

-

Details here...

+

programming

Where do you live?

Details here...

Why do you want to be a web developer?

From 9f64dfe383f3062ef258397b512d10afa415e470 Mon Sep 17 00:00:00 2001 From: jbrestarted <88128662+jbrestarted@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:20:56 -0700 Subject: [PATCH 4/4] Add Beatsmith: MPC + Elektron hip-hop production copilot (#1) * Scaffold Next.js + TypeScript + Tailwind project and core domain types Adds project config (Next 14, TS, Tailwind dark studio theme), gitignore updates, and the core type system (lib/types.ts) for the MPC + Elektron hip-hop production copilot. https://claude.ai/code/session_01JbAYny4cRVLnxpbGmvbwB9 * Implement Beatsmith: MPC + Elektron hip-hop production copilot MVP Adds a local-first Next.js app that guides users through a 15-stage hip-hop production workflow built around the MPC Live III (master clock + arranger), Analog Rytm MKII (drums), and Analog Four MKII (bass/melody). Features: - 15-stage guided workflow with per-device guidance, creative option cards, context-aware idea generator, checklists, notes, and mark-complete controls - 11 hip-hop substyles with BPM/groove/drum/bass/harmony/effects/arrangement characteristics and device-specific suggestions - 166 seeded creative options (15+ per category) across drums, bass, chords, melody, samples, textures, effects, transitions, arrangement, automation, mix - Rule-based idea generator scored by substyle, stage, BPM, mood, devices - Arrangement builder with energy-coded timeline and per-section editors - Editable hardware/MIDI/clock routing planner (defaults, not asserted specs) - Song completion checklist, local persistence, JSON backup export/import - Dark-mode studio UI; strong TypeScript types throughout Builds clean, lint passes, all routes return 200. https://claude.ai/code/session_01JbAYny4cRVLnxpbGmvbwB9 --------- Co-authored-by: Claude --- .eslintrc.json | 3 + .gitignore | 30 +- README.md | 145 +- app/globals.css | 65 + app/layout.tsx | 27 + app/page.tsx | 105 + app/projects/[id]/arrangement/page.tsx | 100 + app/projects/[id]/hardware/page.tsx | 23 + app/projects/[id]/ideas/page.tsx | 114 + app/projects/[id]/page.tsx | 153 + app/projects/[id]/workflow/page.tsx | 82 + app/projects/new/page.tsx | 165 + app/projects/page.tsx | 45 + app/settings/page.tsx | 134 + components/ArrangementSectionEditor.tsx | 149 + components/ArrangementTimeline.tsx | 70 + components/Checklist.tsx | 51 + components/CreativeOptionCard.tsx | 73 + components/DeviceGuidanceCard.tsx | 23 + components/HardwareRoutingPlanner.tsx | 166 + components/IdeaGeneratorPanel.tsx | 89 + components/NotesPanel.tsx | 47 + components/ProductionStagePanel.tsx | 264 + components/ProjectCard.tsx | 49 + components/ProjectGate.tsx | 51 + components/ProjectNav.tsx | 37 + components/ProjectSettingsForm.tsx | 128 + components/Sidebar.tsx | 72 + components/SongCompletionChecklist.tsx | 40 + components/StageStepper.tsx | 56 + components/SubstyleSelector.tsx | 40 + components/ui.tsx | 102 + lib/creativeOptions.ts | 920 ++++ lib/devices.ts | 85 + lib/ideaGenerator.ts | 121 + lib/projectFactory.ts | 108 + lib/stages.ts | 392 ++ lib/store.tsx | 113 + lib/substyles.ts | 214 + lib/types.ts | 174 + next.config.mjs | 6 + package-lock.json | 6193 +++++++++++++++++++++++ package.json | 28 + postcss.config.js | 6 + tailwind.config.ts | 43 + tsconfig.json | 23 + 46 files changed, 11120 insertions(+), 4 deletions(-) create mode 100644 .eslintrc.json create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 app/projects/[id]/arrangement/page.tsx create mode 100644 app/projects/[id]/hardware/page.tsx create mode 100644 app/projects/[id]/ideas/page.tsx create mode 100644 app/projects/[id]/page.tsx create mode 100644 app/projects/[id]/workflow/page.tsx create mode 100644 app/projects/new/page.tsx create mode 100644 app/projects/page.tsx create mode 100644 app/settings/page.tsx create mode 100644 components/ArrangementSectionEditor.tsx create mode 100644 components/ArrangementTimeline.tsx create mode 100644 components/Checklist.tsx create mode 100644 components/CreativeOptionCard.tsx create mode 100644 components/DeviceGuidanceCard.tsx create mode 100644 components/HardwareRoutingPlanner.tsx create mode 100644 components/IdeaGeneratorPanel.tsx create mode 100644 components/NotesPanel.tsx create mode 100644 components/ProductionStagePanel.tsx create mode 100644 components/ProjectCard.tsx create mode 100644 components/ProjectGate.tsx create mode 100644 components/ProjectNav.tsx create mode 100644 components/ProjectSettingsForm.tsx create mode 100644 components/Sidebar.tsx create mode 100644 components/SongCompletionChecklist.tsx create mode 100644 components/StageStepper.tsx create mode 100644 components/SubstyleSelector.tsx create mode 100644 components/ui.tsx create mode 100644 lib/creativeOptions.ts create mode 100644 lib/devices.ts create mode 100644 lib/ideaGenerator.ts create mode 100644 lib/projectFactory.ts create mode 100644 lib/stages.ts create mode 100644 lib/store.tsx create mode 100644 lib/substyles.ts create mode 100644 lib/types.ts create mode 100644 next.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..bffb357a7 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore index 496ee2ca6..9f784c2e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,29 @@ -.DS_Store \ No newline at end of file +.DS_Store + +# dependencies +/node_modules +/.pnp +.pnp.js + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# typescript +*.tsbuildinfo +next-env.d.ts + +# env +.env*.local +.env diff --git a/README.md b/README.md index 3e9e16408..d28331bf5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,143 @@ -## About Me +# πŸŽ›οΈ Beatsmith β€” MPC Γ— Elektron Hip-Hop Production Copilot -* This website is a static HTML website using semantic tags and minimal CSS. -* It is currently hosted on Github Pages. +A **local-first web app** that guides you through the complete hip-hop production +process on a hardware-centered setup: + +- **Akai MPC Live III** β€” master clock, main sequencer, arranger, sampler & song hub +- **Elektron Analog Rytm MKII** β€” drums, percussion, fills, parameter locks, analog drum design +- **Elektron Analog Four MKII** β€” bass, chords, leads, pads, textures & analog melodic synthesis + +It is **not** a generic note-taking app. It's a structured production assistant built around the +MPC-as-master-clock-and-arranger workflow, continuously answering _"what should I create next, on +which device, and what creative options could work?"_ + +--- + +## ✨ What it does + +- **15-stage guided workflow** β€” Project Setup β†’ Tempo/Groove β†’ Reference β†’ Drums β†’ Bass β†’ Harmony + β†’ Hooks β†’ Texture β†’ Effects β†’ Arrangement β†’ Performance Automation β†’ Transitions β†’ Mix Prep β†’ + Export β†’ Final Review. Each stage gives you an objective, **per-device guidance** (MPC / Rytm / A4), + creative option cards, an idea generator, a checklist, notes, and a "mark complete" control. +- **11 hip-hop substyles** with BPM ranges, groove feel, drum/bass/harmony characteristics, effects + & arrangement tendencies, and device-specific suggestions (boom bap, trap, lo-fi, experimental, + dark cinematic, west coast, east coast, southern bounce, grimy underground, soul sample-based, + minimalist drum-machine). +- **Rule-based creative idea generator** β€” context-aware ideas based on substyle, stage, BPM, mood, + active devices, and what you've already saved. Hit **Generate more** for endless variations. +- **166 seed creative options** (15+ per category) across drums, bass, chords, melody, samples, + textures, effects, transitions, arrangement, performance automation, and mix prep β€” each with + recommended device, difficulty, style fit, when-to-use, implementation steps, and variations. +- **Arrangement builder** β€” a visual energy-coded timeline plus per-section editors for bars, active + devices, drum/bass/melody activity, energy, and notes on mutes/fills/drops/automation. +- **Hardware workflow planner** β€” editable device roles, MIDI channels, clock roles, audio routing, + and pattern-to-song / performance-capture strategies. Everything is an **editable assumption**. +- **Song completion checklist** + **local persistence** + **JSON backup export/import**. + +--- + +## 🧱 Tech stack + +| Layer | Choice | +| ------------ | ---------------------------------------- | +| Framework | Next.js 14 (App Router) | +| Language | TypeScript (strict) | +| Styling | Tailwind CSS (dark-mode-first) | +| State / data | React Context + `localStorage` (no auth, no backend) | + +--- + +## πŸš€ Getting started + +```bash +npm install +npm run dev # http://localhost:3000 +``` + +Other scripts: + +```bash +npm run build # production build (also type-checks) +npm run start # serve the production build +npm run lint # eslint +``` + +> Requires Node 18+ (developed on Node 22). + +--- + +## πŸ—ΊοΈ Architecture overview + +``` +app/ # Next.js App Router pages (all client components) + page.tsx # Home / dashboard + projects/ # list Β· new Β· [id] overview + projects/[id]/workflow # the 15-stage guided workflow (centerpiece) + projects/[id]/arrangement # timeline + section editors + projects/[id]/ideas # idea generator + full creative library + projects/[id]/hardware # editable routing/clock/MIDI planner + settings/ # data export/import, default rig reference + +components/ # ProjectCard, StageStepper, ProductionStagePanel, + # DeviceGuidanceCard, CreativeOptionCard, IdeaGeneratorPanel, + # Checklist, ArrangementTimeline, ArrangementSectionEditor, + # HardwareRoutingPlanner, SongCompletionChecklist, NotesPanel, + # SubstyleSelector, ProjectSettingsForm, ProjectNav, ProjectGate, ui + +lib/ + types.ts # strong domain types (Project, Device, ProductionStage, + # CreativeOption, ArrangementSection, HipHopSubstyle, + # ChecklistItem, HardwareRoutingProfile, …) + substyles.ts # 11 substyle definitions + creativeOptions.ts # 166 seed creative options (15+ per category) + stages.ts # the 15 production stages + completion checklist + devices.ts # default (editable) device profiles + routing + ideaGenerator.ts # rule-based, context-aware idea scoring/generation + projectFactory.ts # creates a fully-seeded project + store.tsx # localStorage-backed React context (CRUD + cross-tab sync) +``` + +**Data flow:** `StoreProvider` (in `app/layout.tsx`) hydrates projects from `localStorage`, exposes +CRUD via `useStore()`, and re-persists on every change. Pages resolve a project through +`ProjectGate` (which handles the hydration window) and mutate it with `updateProject(id, updater)`. + +**Persistence is intentionally swappable** β€” replacing the load/save functions in `lib/store.tsx` +with `fetch` calls or a SQLite-backed API route is the entire migration path to a server backend. + +--- + +## πŸ”§ Design assumptions (all editable in-app) + +These are **sensible defaults, not verified hardware specs.** The app never hard-codes hardware +behavior as fact β€” you can edit device capabilities, MIDI channels, clock roles, and routing per +project on the **Hardware** page. + +- MPC Live III is the **master clock** and sends MIDI clock + start/stop. +- Rytm & A4 **receive** clock + transport (CLOCK/TRANSPORT receive on the Elektrons). +- MPC MIDI out β†’ Rytm and A4 (default A4 on ch 1–4, Rytm on its Auto channel β€” verify your config). +- Rytm = drums; A4 = bass/melody; MPC = arranger + sampler + audio capture of the external gear. +- Default new-project BPM is the midpoint of the chosen substyle's range; swing uses the substyle + default. + +If your routing differs (USB vs DIN, different channels, A4 as clock, etc.), just edit it β€” the +guidance text and planner are data, not assumptions baked into code. + +--- + +## πŸ›£οΈ Future enhancements + +- **WebMIDI** β€” send real MIDI clock/transport and program changes to the hardware from the browser. +- **MIDI clock diagnostics** β€” verify sync, jitter, and latency between MPC and the Elektrons. +- **AI-assisted generation** β€” swap the rule-based generator for an LLM that proposes full + patterns/progressions from the project context. +- **Sample library tagging** β€” catalog chops/one-shots/breaks with key/BPM/mood tags and surface + them inside relevant stages. +- **Export templates** β€” generate stem/track naming sheets and session recall docs automatically. +- **SQLite / server sync** β€” optional account-based sync by swapping the persistence layer. + +--- + +## πŸ“¦ Notes + +The repository's previous static "About Me" page (`index.html`, `css/`, `img/`) is left untouched; +the Next.js app lives alongside it and is the active project. diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 000000000..084fadd1c --- /dev/null +++ b/app/globals.css @@ -0,0 +1,65 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + color-scheme: dark; +} + +html, +body { + background-color: #070809; + color: #e7eaf0; +} + +body { + font-feature-settings: "cv02", "cv03", "cv04", "cv11"; + -webkit-font-smoothing: antialiased; +} + +/* Thin custom scrollbar for the studio feel */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} +::-webkit-scrollbar-track { + background: #0c0e12; +} +::-webkit-scrollbar-thumb { + background: #272e3b; + border-radius: 999px; + border: 2px solid #0c0e12; +} +::-webkit-scrollbar-thumb:hover { + background: #3a4456; +} + +@layer components { + .card { + @apply rounded-xl border border-ink-700 bg-ink-850 shadow-card; + } + .card-hover { + @apply transition hover:border-ink-600 hover:bg-ink-800; + } + .btn { + @apply inline-flex items-center justify-center gap-2 rounded-lg px-3.5 py-2 text-sm font-medium transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 disabled:cursor-not-allowed disabled:opacity-50; + } + .btn-primary { + @apply btn bg-accent text-ink-950 hover:bg-accent-soft; + } + .btn-ghost { + @apply btn border border-ink-700 bg-ink-800 text-gray-200 hover:border-ink-600 hover:bg-ink-700; + } + .btn-subtle { + @apply btn text-gray-300 hover:bg-ink-800; + } + .input { + @apply w-full rounded-lg border border-ink-700 bg-ink-900 px-3 py-2 text-sm text-gray-100 placeholder:text-gray-500 focus:border-accent/60 focus:outline-none focus:ring-1 focus:ring-accent/40; + } + .label { + @apply mb-1.5 block text-xs font-medium uppercase tracking-wide text-gray-400; + } + .chip { + @apply inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 000000000..8939f2488 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { StoreProvider } from "@/lib/store"; +import { Sidebar } from "@/components/Sidebar"; + +export const metadata: Metadata = { + title: "Beatsmith β€” MPC + Elektron Hip-Hop Copilot", + description: + "A guided hip-hop production assistant for the Akai MPC Live III, Elektron Analog Rytm MKII, and Analog Four MKII.", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + +
+ +
+
{children}
+
+
+
+ + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 000000000..919d7d225 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,105 @@ +"use client"; + +import Link from "next/link"; +import { useStore } from "@/lib/store"; +import { ProjectCard } from "@/components/ProjectCard"; +import { SectionHeading } from "@/components/ui"; + +const DEVICES = [ + { tag: "MPC Live III", role: "Master clock Β· arranger Β· sampler", cls: "text-mpc border-mpc/40 bg-mpc/10" }, + { tag: "Analog Rytm MKII", role: "Drums Β· percussion Β· fills", cls: "text-rytm border-rytm/40 bg-rytm/10" }, + { tag: "Analog Four MKII", role: "Bass Β· chords Β· leads Β· texture", cls: "text-a4 border-a4/40 bg-a4/10" }, +]; + +export default function HomePage() { + const { projects, hydrated } = useStore(); + const recent = projects.slice(0, 3); + + return ( +
+ {/* Hero */} +
+
+

+ Hip-hop production copilot +

+

+ From idea to arrangement to mix prep β€” guided for your MPC + Elektron rig. +

+

+ A structured, 15-stage workflow that tells you what to build next, what each + device should do, and gives you endless context-aware creative options along the way. +

+
+ + οΌ‹ Start a new beat + + + View projects + +
+ +
+ {DEVICES.map((d) => ( +
+
{d.tag}
+
{d.role}
+
+ ))} +
+
+
+ + {/* Recent projects */} +
+ + All projects β†’ + + } + /> + {!hydrated ? ( +
Loading…
+ ) : recent.length === 0 ? ( +
+
🎚️
+

No projects yet

+

+ Create your first production and the copilot will guide you stage by stage. +

+ + οΌ‹ Start a new beat + +
+ ) : ( +
+ {recent.map((p) => ( + + ))} +
+ )} +
+ + {/* How it works */} +
+ +
+ {[ + { n: "01", t: "Set the target", b: "Pick a substyle, BPM, key, and mood. The app seeds smart defaults for your rig." }, + { n: "02", t: "Work the stages", b: "15 guided stages from drums to mix prep β€” each with per-device actions and idea cards." }, + { n: "03", t: "Arrange & finish", b: "Build sections on the timeline, capture performances, and run the completion checklist." }, + ].map((s) => ( +
+
{s.n}
+

{s.t}

+

{s.b}

+
+ ))} +
+
+
+ ); +} diff --git a/app/projects/[id]/arrangement/page.tsx b/app/projects/[id]/arrangement/page.tsx new file mode 100644 index 000000000..8828bd65c --- /dev/null +++ b/app/projects/[id]/arrangement/page.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useState } from "react"; +import { useParams } from "next/navigation"; +import type { ArrangementSection, Project } from "@/lib/types"; +import { useStore } from "@/lib/store"; +import { uid } from "@/lib/projectFactory"; +import { getSubstyle } from "@/lib/substyles"; +import { ProjectGate } from "@/components/ProjectGate"; +import { ArrangementTimeline } from "@/components/ArrangementTimeline"; +import { ArrangementSectionEditor } from "@/components/ArrangementSectionEditor"; +import { SectionHeading } from "@/components/ui"; + +const SECTION_PRESETS = ["Intro", "Verse", "Pre-Hook", "Hook", "Bridge", "Breakdown", "Drop", "Outro"]; + +export default function ArrangementPage() { + const params = useParams<{ id: string }>(); + return ( + + {(project) => } + + ); +} + +function ArrangementBody({ project }: { project: Project }) { + const { updateProject } = useStore(); + const [activeId, setActiveId] = useState(project.sections[0]?.id); + const style = getSubstyle(project.substyle); + + function setSections(next: ArrangementSection[]) { + updateProject(project.id, (p) => ({ ...p, sections: next })); + } + + function updateSection(id: string, next: ArrangementSection) { + setSections(project.sections.map((s) => (s.id === id ? next : s))); + } + + function removeSection(id: string) { + setSections(project.sections.filter((s) => s.id !== id)); + } + + function addSection(name: string) { + const isHook = /hook|drop|chorus/i.test(name); + const section: ArrangementSection = { + id: uid("sec"), + name, + bars: isHook ? 8 : 16, + activeDevices: ["mpc", "rytm", "a4"], + drumDensity: isHook ? "full" : "medium", + bassActivity: isHook ? "active" : "groove", + melodicActivity: isHook ? "full" : "motif", + energy: isHook ? 5 : 3, + notes: "", + }; + setSections([...project.sections, section]); + setActiveId(section.id); + } + + return ( +
+ + + + +
+
+ Add a section +
+
+ {SECTION_PRESETS.map((name) => ( + + ))} +
+
+ + {project.sections.length === 0 ? ( +
+ No sections yet β€” add one above to start building your arrangement. +
+ ) : ( +
+ {project.sections.map((s) => ( +
setActiveId(s.id)}> + updateSection(s.id, next)} + onRemove={() => removeSection(s.id)} + /> +
+ ))} +
+ )} +
+ ); +} diff --git a/app/projects/[id]/hardware/page.tsx b/app/projects/[id]/hardware/page.tsx new file mode 100644 index 000000000..58c6684b7 --- /dev/null +++ b/app/projects/[id]/hardware/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { ProjectGate } from "@/components/ProjectGate"; +import { HardwareRoutingPlanner } from "@/components/HardwareRoutingPlanner"; +import { SectionHeading } from "@/components/ui"; + +export default function HardwarePage() { + const params = useParams<{ id: string }>(); + return ( + + {(project) => ( +
+ + +
+ )} +
+ ); +} diff --git a/app/projects/[id]/ideas/page.tsx b/app/projects/[id]/ideas/page.tsx new file mode 100644 index 000000000..f910207c6 --- /dev/null +++ b/app/projects/[id]/ideas/page.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useState } from "react"; +import { useParams } from "next/navigation"; +import type { CreativeCategory, Project } from "@/lib/types"; +import { useStore } from "@/lib/store"; +import { uid } from "@/lib/projectFactory"; +import { CATEGORY_LABELS } from "@/lib/stages"; +import { optionsByCategory } from "@/lib/creativeOptions"; +import type { GeneratedIdea } from "@/lib/ideaGenerator"; +import { ProjectGate } from "@/components/ProjectGate"; +import { IdeaGeneratorPanel } from "@/components/IdeaGeneratorPanel"; +import { CreativeOptionCard } from "@/components/CreativeOptionCard"; +import { SectionHeading } from "@/components/ui"; +import type { CreativeOption } from "@/lib/types"; + +const CATEGORIES = Object.keys(CATEGORY_LABELS) as CreativeCategory[]; + +export default function IdeasPage() { + const params = useParams<{ id: string }>(); + return ( + {(project) => } + ); +} + +function IdeasBody({ project }: { project: Project }) { + const { updateProject } = useStore(); + const [category, setCategory] = useState("drums"); + + const savedTitles = new Set(project.decisions.map((d) => d.text.split(" β€” ")[0])); + + function saveIdea(idea: GeneratedIdea) { + updateProject(project.id, (p) => ({ + ...p, + decisions: [ + { id: uid("dec"), category: idea.category, text: idea.text, device: idea.device, createdAt: Date.now() }, + ...p.decisions, + ], + })); + } + + function saveOption(option: CreativeOption) { + updateProject(project.id, (p) => ({ + ...p, + decisions: [ + { + id: uid("dec"), + category: option.category, + text: `${option.title} β€” ${option.description}`, + device: option.recommendedDevice, + createdAt: Date.now(), + }, + ...p.decisions, + ], + })); + } + + const options = optionsByCategory(category); + const onStyle = options.filter( + (o) => o.styleFit === "all" || (o.styleFit as string[]).includes(project.substyle) + ); + const offStyle = options.filter((o) => !onStyle.includes(o)); + const ordered = [...onStyle, ...offStyle]; + + return ( +
+ + + + + {/* Category tabs */} +
+ {CATEGORIES.map((c) => ( + + ))} +
+ +
+

+ {ordered.length} options Β· {onStyle.length} tuned for {project.substyle.replace(/-/g, " ")} +

+
+ {ordered.map((opt) => ( + + ))} +
+
+
+ ); +} diff --git a/app/projects/[id]/page.tsx b/app/projects/[id]/page.tsx new file mode 100644 index 000000000..49155d863 --- /dev/null +++ b/app/projects/[id]/page.tsx @@ -0,0 +1,153 @@ +"use client"; + +import Link from "next/link"; +import { useParams, useRouter } from "next/navigation"; +import { useStore } from "@/lib/store"; +import { ProjectGate } from "@/components/ProjectGate"; +import { ProjectSettingsForm } from "@/components/ProjectSettingsForm"; +import { SongCompletionChecklist } from "@/components/SongCompletionChecklist"; +import { ProgressBar } from "@/components/ui"; +import { STAGES, STAGE_MAP } from "@/lib/stages"; +import { nextRecommendedStageId } from "@/lib/ideaGenerator"; +import { getSubstyle } from "@/lib/substyles"; + +export default function ProjectOverviewPage() { + const params = useParams<{ id: string }>(); + const router = useRouter(); + const { deleteProject } = useStore(); + + return ( + + {(project) => { + const style = getSubstyle(project.substyle); + const completeStages = STAGES.filter( + (s) => project.stageStates[s.id]?.status === "complete" + ).length; + const pct = Math.round((completeStages / STAGES.length) * 100); + const nextId = nextRecommendedStageId(project); + const nextStage = STAGE_MAP[nextId]; + + return ( +
+ {/* Header */} +
+
+

{project.title}

+

+ {project.producerName ? `${project.producerName} Β· ` : ""} + {style.name} Β· {project.bpm} BPM Β· {project.key} {project.scale} + {project.mood ? ` Β· ${project.mood}` : ""} +

+
+ +
+ + {/* Progress + next action */} +
+
+
+

+ Production progress +

+ + {completeStages} / {STAGES.length} stages + +
+ +
+ {STAGES.map((s, i) => { + const st = project.stageStates[s.id]?.status ?? "not-started"; + return ( + + + {st === "complete" ? "βœ“" : i + 1} + + {s.name} + + ); + })} +
+
+ +
+
+

+ Next recommended +

+

{nextStage.name}

+

{nextStage.objective}

+
+ + Continue β†’ + +
+
+ + {/* Quick links */} +
+ {[ + { href: `/projects/${project.id}/workflow`, label: "Workflow", icon: "🎚" }, + { href: `/projects/${project.id}/arrangement`, label: "Arrangement", icon: "πŸ“" }, + { href: `/projects/${project.id}/ideas`, label: "Idea bank", icon: "πŸ’‘" }, + { href: `/projects/${project.id}/hardware`, label: "Hardware", icon: "πŸ”Œ" }, + ].map((q) => ( + + {q.icon} + {q.label} + + ))} +
+ +
+
+ +
+
+ + {project.decisions.length > 0 && ( +
+

+ Saved decisions ({project.decisions.length}) +

+
    + {project.decisions.slice(0, 8).map((d) => ( +
  • + {d.text} +
  • + ))} +
+
+ )} +
+
+
+ ); + }} +
+ ); +} diff --git a/app/projects/[id]/workflow/page.tsx b/app/projects/[id]/workflow/page.tsx new file mode 100644 index 000000000..9359d89c4 --- /dev/null +++ b/app/projects/[id]/workflow/page.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { Suspense, useState } from "react"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; +import { ProjectGate } from "@/components/ProjectGate"; +import { StageStepper } from "@/components/StageStepper"; +import { ProductionStagePanel } from "@/components/ProductionStagePanel"; +import { STAGES, STAGE_MAP } from "@/lib/stages"; +import { nextRecommendedStageId } from "@/lib/ideaGenerator"; +import type { Project } from "@/lib/types"; + +function WorkflowInner() { + const params = useParams<{ id: string }>(); + const router = useRouter(); + const searchParams = useSearchParams(); + const stageParam = searchParams.get("stage"); + + return ( + + {(project) => } + + ); +} + +function WorkflowBody({ + project, + stageParam, + router, +}: { + project: Project; + stageParam: string | null; + router: ReturnType; +}) { + const initial = stageParam && STAGE_MAP[stageParam] ? stageParam : nextRecommendedStageId(project); + const [activeStageId, setActiveStageId] = useState(initial); + const idx = STAGES.findIndex((s) => s.id === activeStageId); + + function select(id: string) { + setActiveStageId(id); + router.replace(`/projects/${project.id}/workflow?stage=${id}`, { scroll: false }); + } + + return ( +
+
+

+ Production stages +

+ +
+ +
+ + +
+ + +
+
+
+ ); +} + +export default function WorkflowPage() { + return ( + Loading…}> + + + ); +} diff --git a/app/projects/new/page.tsx b/app/projects/new/page.tsx new file mode 100644 index 000000000..0ebf951b8 --- /dev/null +++ b/app/projects/new/page.tsx @@ -0,0 +1,165 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; +import type { SubstyleId } from "@/lib/types"; +import { useStore } from "@/lib/store"; +import { createProject } from "@/lib/projectFactory"; +import { getSubstyle } from "@/lib/substyles"; +import { SubstyleSelector } from "@/components/SubstyleSelector"; +import { SectionHeading } from "@/components/ui"; + +export default function NewProjectPage() { + const router = useRouter(); + const { addProject } = useStore(); + + const [title, setTitle] = useState(""); + const [producerName, setProducerName] = useState(""); + const [substyle, setSubstyle] = useState(null); + const [bpm, setBpm] = useState(""); + const [key, setKey] = useState("C"); + const [scale, setScale] = useState("Minor"); + const [mood, setMood] = useState(""); + const [referenceTracks, setReferenceTracks] = useState(""); + + const style = useMemo(() => (substyle ? getSubstyle(substyle) : null), [substyle]); + + function handleSubstyle(id: SubstyleId) { + setSubstyle(id); + const s = getSubstyle(id); + // Suggest a BPM if the user hasn't set one. + if (bpm === "") setBpm(Math.round((s.bpmRange[0] + s.bpmRange[1]) / 2)); + } + + function handleCreate() { + if (!substyle || !title.trim()) return; + const project = createProject({ + title: title.trim(), + producerName: producerName.trim(), + substyle, + bpm: bpm === "" ? undefined : Number(bpm), + key, + scale, + mood: mood.trim(), + referenceTracks: referenceTracks.trim(), + }); + addProject(project); + router.push(`/projects/${project.id}/workflow`); + } + + const canCreate = !!substyle && title.trim().length > 0; + + return ( +
+ + +
+
+
+
+ + setTitle(e.target.value)} + autoFocus + /> +
+
+ + setProducerName(e.target.value)} + /> +
+
+
+ +
+ + +
+ + {style && ( +
+
{style.name}
+

{style.grooveFeel}

+
+ + BPM {style.bpmRange[0]}–{style.bpmRange[1]} + + Swing ~{style.defaultSwing}% +
+
+ )} + +
+
+
+ + setBpm(e.target.value === "" ? "" : Number(e.target.value))} + /> +
+
+
+ + setKey(e.target.value)} /> +
+
+ + +
+
+
+ + setMood(e.target.value)} + /> +
+
+ + setReferenceTracks(e.target.value)} + /> +
+
+
+ +
+ + +
+ {!canCreate && ( +

+ Add a title and pick a substyle to continue. +

+ )} +
+
+ ); +} diff --git a/app/projects/page.tsx b/app/projects/page.tsx new file mode 100644 index 000000000..5b39a9205 --- /dev/null +++ b/app/projects/page.tsx @@ -0,0 +1,45 @@ +"use client"; + +import Link from "next/link"; +import { useStore } from "@/lib/store"; +import { ProjectCard } from "@/components/ProjectCard"; +import { SectionHeading } from "@/components/ui"; + +export default function ProjectsPage() { + const { projects, hydrated } = useStore(); + + return ( +
+ + οΌ‹ New project + + } + /> + + {!hydrated ? ( +
Loading…
+ ) : projects.length === 0 ? ( +
+
πŸ—‚οΈ
+

No projects yet

+

+ Start your first beat and the copilot walks you from idea to mix prep. +

+ + οΌ‹ Start a new beat + +
+ ) : ( +
+ {projects.map((p) => ( + + ))} +
+ )} +
+ ); +} diff --git a/app/settings/page.tsx b/app/settings/page.tsx new file mode 100644 index 000000000..81a2aae2b --- /dev/null +++ b/app/settings/page.tsx @@ -0,0 +1,134 @@ +"use client"; + +import { useRef, useState } from "react"; +import { useStore } from "@/lib/store"; +import { DEFAULT_DEVICES, DEFAULT_ROUTING } from "@/lib/devices"; +import { SectionHeading, DeviceBadge } from "@/components/ui"; +import type { Project } from "@/lib/types"; + +export default function SettingsPage() { + const { projects, addProject, hydrated } = useStore(); + const fileRef = useRef(null); + const [msg, setMsg] = useState(null); + + function exportData() { + const blob = new Blob([JSON.stringify(projects, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `beatsmith-projects-${new Date().toISOString().slice(0, 10)}.json`; + a.click(); + URL.revokeObjectURL(url); + } + + function importData(file: File) { + const reader = new FileReader(); + reader.onload = () => { + try { + const data = JSON.parse(String(reader.result)); + const arr: Project[] = Array.isArray(data) ? data : [data]; + const existing = new Set(projects.map((p) => p.id)); + let added = 0; + for (const p of arr) { + if (p && p.id && !existing.has(p.id)) { + addProject(p); + added += 1; + } + } + setMsg(`Imported ${added} project${added === 1 ? "" : "s"}.`); + } catch { + setMsg("Could not parse that file."); + } + }; + reader.readAsText(file); + } + + function clearAll() { + if (confirm("Delete ALL projects from this browser? This cannot be undone.")) { + window.localStorage.removeItem("mpc-elektron-copilot:v1"); + window.location.reload(); + } + } + + return ( +
+ + + {/* Data management */} +
+

+ Your data +

+

+ {hydrated ? `${projects.length} project${projects.length === 1 ? "" : "s"} stored locally.` : "Loading…"}{" "} + Export a backup or move projects between browsers/devices. +

+
+ + + e.target.files?.[0] && importData(e.target.files[0])} + /> + +
+ {msg &&

{msg}

} +
+ + {/* Default rig (reference) */} +
+

+ Default rig assumptions +

+

+ These seed every new project and are fully editable per-project on the Hardware page. + They're sensible defaults β€” not verified hardware specs. +

+
+ {DEFAULT_DEVICES.map((d) => ( +
+
+ +
+

{d.role}

+
+
+
MIDI
+
{d.midiChannel}
+
+
+
Clock
+
{d.clockRole}
+
+
+
+ ))} +
+
+ Clock default:{" "} + {DEFAULT_ROUTING.notes.clock} +
+
+ + {/* About */} +
+

About

+

+ Beatsmith is a local-first hip-hop + production copilot for the Akai MPC Live III, Elektron Analog Rytm MKII, and Analog Four + MKII. No account, no cloud β€” your projects stay in your browser. WebMIDI sync, MIDI clock + diagnostics, and AI-assisted generation are on the roadmap. +

+
+
+ ); +} diff --git a/components/ArrangementSectionEditor.tsx b/components/ArrangementSectionEditor.tsx new file mode 100644 index 000000000..a6d4b3b62 --- /dev/null +++ b/components/ArrangementSectionEditor.tsx @@ -0,0 +1,149 @@ +"use client"; + +import type { ArrangementSection, DeviceId } from "@/lib/types"; +import { DEVICE_META } from "./ui"; +import { NotesPanel } from "./NotesPanel"; + +const DEVICES: DeviceId[] = ["mpc", "rytm", "a4"]; +const DRUM_DENSITY = ["none", "sparse", "medium", "busy", "full"] as const; +const BASS_ACTIVITY = ["none", "sustained", "groove", "active"] as const; +const MELODIC_ACTIVITY = ["none", "pad", "motif", "lead", "full"] as const; + +export function ArrangementSectionEditor({ + section, + onChange, + onRemove, +}: { + section: ArrangementSection; + onChange: (next: ArrangementSection) => void; + onRemove: () => void; +}) { + function set(key: K, value: ArrangementSection[K]) { + onChange({ ...section, [key]: value }); + } + + function toggleDevice(d: DeviceId) { + const has = section.activeDevices.includes(d); + set( + "activeDevices", + has ? section.activeDevices.filter((x) => x !== d) : [...section.activeDevices, d] + ); + } + + return ( +
+
+ set("name", e.target.value)} + /> + +
+ +
+
+ + set("bars", Math.max(1, Number(e.target.value) || 1))} + /> +
+
+ + set("energy", Number(e.target.value) as ArrangementSection["energy"])} + className="mt-2 w-full accent-[#a78bfa]" + /> +
+
+ +
+ +
+ {DEVICES.map((d) => { + const active = section.activeDevices.includes(d); + const m = DEVICE_META[d]; + return ( + + ); + })} +
+
+ +
+ set("bassActivity", v as ArrangementSection["bassActivity"])} + /> + onChange(e.target.value)}> + {options.map((o) => ( + + ))} + +
+ ); +} diff --git a/components/ArrangementTimeline.tsx b/components/ArrangementTimeline.tsx new file mode 100644 index 000000000..5a382deee --- /dev/null +++ b/components/ArrangementTimeline.tsx @@ -0,0 +1,70 @@ +"use client"; + +import type { ArrangementSection } from "@/lib/types"; +import { DEVICE_META } from "./ui"; + +const ENERGY_COLOR = [ + "bg-ink-600", + "bg-sky-700", + "bg-emerald-600", + "bg-amber-500", + "bg-rose-500", +]; + +export function ArrangementTimeline({ + sections, + activeId, + onSelect, +}: { + sections: ArrangementSection[]; + activeId?: string; + onSelect?: (id: string) => void; +}) { + const totalBars = sections.reduce((sum, s) => sum + s.bars, 0) || 1; + + return ( +
+
+ Timeline + {totalBars} bars total +
+
+ {sections.map((s) => { + const widthPct = (s.bars / totalBars) * 100; + const active = s.id === activeId; + return ( + + ); + })} +
+
+ Energy: + {["1 low", "2", "3", "4", "5 peak"].map((l, i) => ( + + {l} + + ))} +
+
+ ); +} diff --git a/components/Checklist.tsx b/components/Checklist.tsx new file mode 100644 index 000000000..01d88dd8e --- /dev/null +++ b/components/Checklist.tsx @@ -0,0 +1,51 @@ +"use client"; + +import type { ChecklistItem } from "@/lib/types"; + +export function Checklist({ + items, + onToggle, +}: { + items: ChecklistItem[]; + onToggle: (id: string) => void; +}) { + const done = items.filter((i) => i.done).length; + + return ( +
+
    + {items.map((item) => ( +
  • + +
  • + ))} +
+ {items.length > 0 && ( +

+ {done} / {items.length} complete +

+ )} +
+ ); +} diff --git a/components/CreativeOptionCard.tsx b/components/CreativeOptionCard.tsx new file mode 100644 index 000000000..17fb62f78 --- /dev/null +++ b/components/CreativeOptionCard.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useState } from "react"; +import type { CreativeOption } from "@/lib/types"; +import { DeviceBadge, DifficultyDot } from "./ui"; + +export function CreativeOptionCard({ + option, + onSave, + saved, +}: { + option: CreativeOption; + onSave?: (option: CreativeOption) => void; + saved?: boolean; +}) { + const [open, setOpen] = useState(false); + + return ( +
+
+

{option.title}

+ +
+ +

{option.description}

+ +
+ + {option.whenToUse} +
+ + {open && ( +
+
+
+ How to do it +
+
    + {option.implementationSteps.map((s, i) => ( +
  1. {s}
  2. + ))} +
+
+
+
+ Variations to try +
+
    + {option.variationIdeas.map((s, i) => ( +
  • {s}
  • + ))} +
+
+
+ )} + +
+ + {onSave && ( + + )} +
+
+ ); +} diff --git a/components/DeviceGuidanceCard.tsx b/components/DeviceGuidanceCard.tsx new file mode 100644 index 000000000..755624f4a --- /dev/null +++ b/components/DeviceGuidanceCard.tsx @@ -0,0 +1,23 @@ +"use client"; + +import type { DeviceId } from "@/lib/types"; +import { DEVICE_META } from "./ui"; + +export function DeviceGuidanceCard({ + device, + guidance, +}: { + device: DeviceId; + guidance: string; +}) { + const m = DEVICE_META[device]; + return ( +
+
+ + {m.full} +
+

{guidance}

+
+ ); +} diff --git a/components/HardwareRoutingPlanner.tsx b/components/HardwareRoutingPlanner.tsx new file mode 100644 index 000000000..85ed85439 --- /dev/null +++ b/components/HardwareRoutingPlanner.tsx @@ -0,0 +1,166 @@ +"use client"; + +import type { Device, Project } from "@/lib/types"; +import { useStore } from "@/lib/store"; +import { DEVICE_META } from "./ui"; +import { NotesPanel } from "./NotesPanel"; + +/** + * Editable hardware planner. Every value here is a DEFAULT ASSUMPTION the user + * can override β€” we never assert verified hardware behavior. + */ +export function HardwareRoutingPlanner({ project }: { project: Project }) { + const { updateProject } = useStore(); + + function updateDevice(id: Device["id"], patch: Partial) { + updateProject(project.id, (p) => ({ + ...p, + devices: p.devices.map((d) => (d.id === id ? { ...d, ...patch } : d)), + })); + } + + function updateRoutingNote(key: keyof Project["routing"]["notes"], value: string) { + updateProject(project.id, (p) => ({ + ...p, + routing: { ...p.routing, notes: { ...p.routing.notes, [key]: value } }, + })); + } + + function toggleClock() { + updateProject(project.id, (p) => ({ + ...p, + routing: { ...p.routing, sendsMidiClock: !p.routing.sendsMidiClock }, + })); + } + + const routingFields: { key: keyof Project["routing"]["notes"]; label: string }[] = [ + { key: "clock", label: "Master clock & transport" }, + { key: "midiOut", label: "MIDI out β†’ Elektrons" }, + { key: "audio", label: "Audio recording / monitoring" }, + { key: "arrangementCapture", label: "Arrangement capture strategy" }, + { key: "patternToSong", label: "Pattern-to-song workflow" }, + { key: "performanceCapture", label: "Performance capture workflow" }, + ]; + + return ( +
+
+ Editable assumptions. These are + sensible defaults for an MPC-as-master setup β€” not verified hardware specs. + Confirm against your own MIDI config and adjust freely. +
+ + {/* Clock summary */} +
+

+ Clock & sync +

+ +
+ + {/* Devices */} +
+ {project.devices.map((device) => { + const m = DEVICE_META[device.id]; + return ( +
+
+ + {device.name} +
+ + +