Muse: AI chat app
A calm, reading-first AI chat app in five pages: a chat with reasoning, sources, a document panel and a model picker, a new-chat screen, projects, a library of everything made, and settings.
// Ballmac UI: Muse template fonts. https://ui.ballmac.com/templates/template-muse
import { DM_Sans, Source_Serif_4 } from "next/font/google"
export const museSans = DM_Sans({ variable: "--muse-sans", subsets: ["latin"], display: "swap", preload: false })
/** The reading face for replies: a book serif that stays comfortable for long answers. */
export const museSerif = Source_Serif_4({ variable: "--muse-serif", subsets: ["latin"], display: "swap", preload: false })// Ballmac UI: Muse sample data. https://ui.ballmac.com/templates/template-muse
import type { ModelOption } from "@/components/ballmac/model-picker"
export const models: ModelOption[] = [
{ id: "muse-5", name: "Muse 5", provider: "Muse", description: "The most capable model. Best for long, careful work.", capabilities: ["vision", "reasoning", "tools"], contextWindow: 500000, cost: 3, isNew: true },
{ id: "muse-5-fast", name: "Muse 5 Fast", provider: "Muse", description: "Quick answers for everyday questions.", capabilities: ["vision", "tools"], contextWindow: 200000, cost: 1 },
{ id: "muse-4-writer", name: "Muse 4 Writer", provider: "Muse", description: "Tuned for drafting, editing and tone.", capabilities: ["reasoning"], contextWindow: 200000, cost: 2 },
]
export type MuseChatItem = { id: string; title: string; group: "Today" | "Yesterday" | "Previous 7 days"; project?: string }
export const history: MuseChatItem[] = [
{ id: "c1", title: "Q4 plan for the design team", group: "Today", project: "Design team" },
{ id: "c2", title: "Rename the onboarding emails", group: "Today" },
{ id: "c3", title: "Explain index funds like I’m ten", group: "Yesterday" },
{ id: "c4", title: "Recipe: weeknight miso aubergine", group: "Yesterday" },
{ id: "c5", title: "Review my cover letter", group: "Previous 7 days" },
{ id: "c6", title: "Trip to Lisbon in March", group: "Previous 7 days", project: "Travel" },
{ id: "c7", title: "SQL: cohort retention query", group: "Previous 7 days", project: "Analytics" },
]
export type MuseProject = { id: string; name: string; description: string; tone: number; chats: number; files: { name: string; size: string }[]; instructions: string }
export const projects: MuseProject[] = [
{ id: "design", name: "Design team", description: "Roadmaps, critique notes and the weekly update.", tone: 1, chats: 14, files: [{ name: "Q3 retro.md", size: "12 KB" }, { name: "Brand guidelines.pdf", size: "2.1 MB" }, { name: "Research summary.docx", size: "88 KB" }], instructions: "Write in plain, warm language. Keep updates under 200 words. Always end with the next step and who owns it." },
{ id: "travel", name: "Travel", description: "Itineraries, bookings and packing lists.", tone: 2, chats: 6, files: [{ name: "Lisbon.pdf", size: "640 KB" }, { name: "Packing list.md", size: "3 KB" }], instructions: "Prefer walkable neighbourhoods and small restaurants. Give times in 24-hour format." },
{ id: "analytics", name: "Analytics", description: "SQL, dashboards and metric definitions.", tone: 3, chats: 22, files: [{ name: "Schema.sql", size: "34 KB" }, { name: "Metric glossary.md", size: "9 KB" }, { name: "Cohorts.csv", size: "1.4 MB" }, { name: "Notes.txt", size: "2 KB" }], instructions: "Use Postgres syntax. Explain each query in two sentences before showing it." },
{ id: "writing", name: "Writing", description: "Essays, drafts and edits in progress.", tone: 4, chats: 9, files: [{ name: "Essay draft.md", size: "28 KB" }], instructions: "Be a candid editor. Point out the weakest paragraph first." },
]
export type MuseArtifact = { id: string; title: string; kind: "Document" | "Code" | "Data"; updated: string; lines: string[]; body: string; filename: string; language: string }
export const artifacts: MuseArtifact[] = [
{ id: "a1", title: "Q4 design plan", kind: "Document", updated: "Today", lines: ["Goals", "Three bets", "Risks and owners"], filename: "q4-design-plan.md", language: "text", body: "# Q4 design plan\n\n## Goals\n- Ship the new onboarding flow by Oct 31\n- Cut design-to-dev handoff time in half\n- Run two customer research rounds\n\n## Three bets\n1. Onboarding rebuild\n2. Component library v2\n3. Research ops\n\n## Risks and owners\n- Scope creep on onboarding (Priya)\n- Engineering capacity in November (Dev)" },
{ id: "a2", title: "Cohort retention query", kind: "Code", updated: "3 days ago", lines: ["WITH cohorts AS (", " SELECT user_id", " FROM signups"], filename: "retention.sql", language: "text", body: "WITH cohorts AS (\n SELECT user_id, date_trunc('week', created_at) AS cohort\n FROM signups\n)\nSELECT cohort,\n count(*) AS users,\n count(*) FILTER (WHERE active_week_4) AS retained\nFROM cohorts\nJOIN activity USING (user_id)\nGROUP BY 1\nORDER BY 1;" },
{ id: "a3", title: "Lisbon itinerary", kind: "Document", updated: "Last week", lines: ["Day 1: Alfama", "Day 2: Belém", "Day 3: Sintra"], filename: "lisbon.md", language: "text", body: "# Lisbon, 4 days\n\n**Day 1** Alfama by foot, lunch at Taberna da Rua das Flores.\n**Day 2** Belém, pastéis, the Tower.\n**Day 3** Sintra by train, back by 18:00.\n**Day 4** Slow morning, LX Factory." },
{ id: "a4", title: "Onboarding email copy", kind: "Document", updated: "Last week", lines: ["Subject: Welcome to Fieldnote", "Hi {{name}},", "Here is how to start"], filename: "emails.md", language: "text", body: "Subject: Welcome to Fieldnote\n\nHi {{name}},\n\nThanks for joining. Here is the one thing to do first: write your first note." },
{ id: "a5", title: "Weekly metrics", kind: "Data", updated: "2 weeks ago", lines: ["week,signups,active", "36,412,288", "37,447,301"], filename: "metrics.csv", language: "text", body: "week,signups,active\n36,412,288\n37,447,301\n38,501,336\n39,478,342" },
{ id: "a6", title: "Debounce hook", kind: "Code", updated: "2 weeks ago", lines: ["export function useDebounce<T>(", " value: T, delay = 300", ") {"], filename: "use-debounce.ts", language: "typescript", body: "export function useDebounce<T>(value: T, delay = 300) {\n const [debounced, setDebounced] = useState(value)\n useEffect(() => {\n const id = setTimeout(() => setDebounced(value), delay)\n return () => clearTimeout(id)\n }, [value, delay])\n return debounced\n}" },
]
export const memories = [
"Works on the design team at Fieldnote Goods",
"Prefers short answers with one clear next step",
"Based in Portland, writes in US English",
"Vegetarian; cooks on weeknights",
"Planning a trip to Lisbon in March",
]
export const suggestions = [
{ label: "Plan my week", prompt: "Help me plan my week. I have three deadlines and a trip on Friday.", description: "Turn a messy to-do list into a calm schedule" },
{ label: "Explain something", prompt: "Explain how index funds work, like I’m ten.", description: "A clear answer with no jargon" },
{ label: "Draft an email", prompt: "Draft a kind but firm email asking a client to pay an overdue invoice.", description: "Tone, structure and a subject line" },
{ label: "Review my writing", prompt: "Be a candid editor: tell me what is weak in my opening paragraph.", description: "Honest feedback, weakest part first" },
]
/** Scripted replies, cycled so a demo conversation always has something sensible to say. */
export const replies = [
"Here is a calm way to think about it. Start with what is fixed, such as meetings and the Friday trip, then place the deadlines in the gaps with the hardest one first, while your energy is highest.\n\nI would put the biggest deadline on Tuesday morning, leave Thursday light for surprises, and finish anything that can’t slip by Wednesday night. Want me to turn this into a day-by-day list?",
"Good question. The short version: an index fund is a basket that owns a tiny slice of hundreds of companies, so you are not betting on any single one.\n\nWhen the companies in the basket do well, so does your slice. Because nobody is picking winners, the fees are very low, which is most of why they work.",
"Of course. Here is a version that stays warm while being clear about the date:\n\nSubject: Quick reminder about invoice 1042\n\nHi Sam, I hope the project is going well. A note that invoice 1042 was due on the 15th. Could you let me know when to expect payment? Happy to resend it if that helps.",
]// Ballmac UI: Muse template shell. https://ui.ballmac.com/templates/template-muse
"use client"
import * as React from "react"
import { Library, MessageSquarePlus, Menu, FolderKanban, Settings } from "lucide-react"
import { Sheet, SheetContent, SheetDescription, SheetTitle } from "@/components/ballmac/sheet"
import { history } from "@/components/ballmac/templates/muse/muse-data"
import { museSans, museSerif } from "@/components/ballmac/templates/muse/muse-fonts"
import { cn } from "@/lib/utils"
type MusePage = "chat" | "new" | "projects" | "library" | "settings"
type MuseHrefs = Record<MusePage, string>
const defaultHrefs: MuseHrefs = { chat: "/muse", new: "/muse/new", projects: "/muse/projects", library: "/muse/library", settings: "/muse/settings" }
/** Muse's palette: lilac-tinted paper with plum ink and one coral. Calm, roomy and easy on the eyes. */
const museCss = `
.muse-theme,body:not(:has([data-gallery])):has(.muse-theme){--background:oklch(0.978 0.008 300);--foreground:oklch(0.24 0.03 300);--card:oklch(0.995 0.004 300);--card-foreground:oklch(0.24 0.03 300);--popover:oklch(0.995 0.004 300);--popover-foreground:oklch(0.24 0.03 300);--primary:oklch(0.3 0.04 300);--primary-foreground:oklch(0.98 0.006 300);--secondary:oklch(0.95 0.012 300);--secondary-foreground:oklch(0.24 0.03 300);--muted:oklch(0.95 0.012 300);--muted-foreground:oklch(0.48 0.025 300);--accent:oklch(0.935 0.018 300);--accent-foreground:oklch(0.24 0.03 300);--border:oklch(0.24 0.03 300 / 12%);--input:oklch(0.24 0.03 300 / 17%);--ring:oklch(0.55 0.16 28);--surface:oklch(0.962 0.01 300);--destructive:oklch(0.54 0.21 25);--chart-1:oklch(0.52 0.16 28);--chart-2:oklch(0.5 0.12 160);--chart-3:oklch(0.62 0.13 75);--chart-4:oklch(0.5 0.15 300);--chart-5:oklch(0.5 0.1 240);--radius:0.875rem}
.dark .muse-theme,.dark body:not(:has([data-gallery])):has(.muse-theme){--background:oklch(0.18 0.014 300);--foreground:oklch(0.95 0.008 300);--card:oklch(0.215 0.016 300);--card-foreground:oklch(0.95 0.008 300);--popover:oklch(0.235 0.018 300);--popover-foreground:oklch(0.95 0.008 300);--primary:oklch(0.95 0.008 300);--primary-foreground:oklch(0.22 0.02 300);--secondary:oklch(0.26 0.018 300);--secondary-foreground:oklch(0.95 0.008 300);--muted:oklch(0.25 0.018 300);--muted-foreground:oklch(0.73 0.02 300);--accent:oklch(0.285 0.02 300);--accent-foreground:oklch(0.95 0.008 300);--border:oklch(1 0 0 / 10%);--input:oklch(1 0 0 / 14%);--ring:oklch(0.74 0.14 30);--surface:oklch(0.2 0.015 300);--destructive:oklch(0.7 0.19 25);--chart-1:oklch(0.76 0.13 30);--chart-2:oklch(0.78 0.13 160);--chart-3:oklch(0.83 0.13 80);--chart-4:oklch(0.75 0.13 300);--chart-5:oklch(0.76 0.1 240)}
body:not(:has([data-gallery])):has(.muse-theme){font-family:var(--muse-sans),ui-sans-serif,system-ui,sans-serif}
`
const nav: { key: MusePage; label: string; icon: typeof Library }[] = [
{ key: "projects", label: "Projects", icon: FolderKanban },
{ key: "library", label: "Library", icon: Library },
{ key: "settings", label: "Settings", icon: Settings },
]
const groups = ["Today", "Yesterday", "Previous 7 days"] as const
function MuseMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 28 28" aria-hidden="true" className={cn("size-7", className)} fill="none">
<circle cx="14" cy="14" r="13" fill="var(--chart-1)" />
<path d="M8 18c1.5-5 3-8 6-8s4.5 3 6 8" stroke="var(--card)" strokeWidth="2.2" strokeLinecap="round" />
<circle cx="14" cy="9" r="1.6" fill="var(--card)" />
</svg>
)
}
function SidebarBody({ page, hrefs, activeChat, onNavigate }: { page: MusePage; hrefs: MuseHrefs; activeChat?: string; onNavigate?: () => void }) {
return (
<div className="flex h-full flex-col">
<div className="flex h-14 items-center gap-2.5 px-4">
<MuseMark />
<span className="text-lg font-semibold tracking-tight">Muse</span>
</div>
<div className="px-3">
<a href={hrefs.new} onClick={onNavigate} aria-current={page === "new" ? "page" : undefined} className="bg-card hover:bg-accent focus-visible:ring-ring/50 flex items-center gap-2 rounded-xl border px-3 py-2.5 text-sm font-medium shadow-xs outline-none transition-colors focus-visible:ring-[3px]">
<MessageSquarePlus className="text-chart-1 size-4" aria-hidden="true" /> New chat
</a>
<nav aria-label="Main" className="mt-3 space-y-0.5">
{nav.map((n) => (
<a key={n.key} href={hrefs[n.key]} onClick={onNavigate} aria-current={page === n.key ? "page" : undefined} className="text-muted-foreground hover:text-foreground hover:bg-accent aria-[current=page]:bg-accent aria-[current=page]:text-foreground focus-visible:ring-ring/50 flex items-center gap-2.5 rounded-xl px-3 py-2 text-sm outline-none transition-colors focus-visible:ring-[3px]">
<n.icon className="size-4" aria-hidden="true" /> {n.label}
</a>
))}
</nav>
</div>
<div className="mt-4 min-h-0 flex-1 overflow-y-auto px-3 pb-3">
{groups.map((g) => (
<section key={g} aria-label={g} className="mb-4">
<h2 className="text-muted-foreground px-3 pb-1.5 text-xs font-medium">{g}</h2>
<ul>
{history.filter((h) => h.group === g).map((h) => (
<li key={h.id}>
<a href={hrefs.chat} onClick={onNavigate} aria-current={h.id === activeChat ? "page" : undefined} className="text-foreground/85 hover:bg-accent aria-[current=page]:bg-accent focus-visible:ring-ring/50 block truncate rounded-xl px-3 py-2 text-sm outline-none transition-colors focus-visible:ring-[3px]">
{h.title}
</a>
</li>
))}
</ul>
</section>
))}
</div>
<div className="border-t p-3">
<div className="flex items-center gap-2.5 rounded-xl p-2">
<span className="bg-chart-4/25 flex size-8 items-center justify-center rounded-full text-xs font-semibold" aria-hidden="true">MK</span>
<span className="min-w-0 flex-1 leading-tight"><span className="block truncate text-sm font-medium">Mina Kovac</span><span className="text-muted-foreground block text-xs">Pro plan</span></span>
</div>
</div>
</div>
)
}
type MuseShellProps = Omit<React.ComponentProps<"div">, "title"> & {
/** The page being shown, so its nav link is marked current. */
page: MusePage
/** Heading in the top bar. */
title: React.ReactNode
/** The chat to mark current in the history list. */
activeChat?: string
/** Override where pages live (used by previews). */
hrefs?: Partial<MuseHrefs>
/** Controls on the right of the top bar. */
actions?: React.ReactNode
/** Lets the content fill the height of the window (the chat does), instead of scrolling the page. */
fill?: boolean
}
/** Muse's frame: a history sidebar, a quiet top bar and room to read. Wrap every Muse page in it. */
function MuseShell({ page, title, activeChat, hrefs: overrides, actions, fill, className, style, children, ...props }: MuseShellProps) {
const hrefs = { ...defaultHrefs, ...overrides }
const [drawer, setDrawer] = React.useState(false)
// Portaled UI (menus, dialogs, sheets) lives outside this element, so <body> gets the font variables too.
React.useEffect(() => {
const classes = [museSans.variable, museSerif.variable].filter(Boolean)
document.body.classList.add(...classes)
return () => document.body.classList.remove(...classes)
}, [])
return (
<div
data-slot="muse"
className={cn("muse-theme bg-background text-foreground flex min-h-dvh", fill && "h-dvh overflow-hidden", museSans.variable, museSerif.variable, className)}
style={{ fontFamily: "var(--muse-sans), ui-sans-serif, system-ui, sans-serif", ...style }}
{...props}
>
<style>{museCss}</style>
<aside className="bg-surface hidden w-64 shrink-0 border-e lg:block">
<SidebarBody page={page} hrefs={hrefs} activeChat={activeChat} />
</aside>
<Sheet open={drawer} onOpenChange={setDrawer}>
<SheetContent side="start" className="w-72 p-0" closeLabel="Close menu">
<SheetTitle className="sr-only">Menu</SheetTitle>
<SheetDescription className="sr-only">Chats, projects and settings</SheetDescription>
<SidebarBody page={page} hrefs={hrefs} activeChat={activeChat} onNavigate={() => setDrawer(false)} />
</SheetContent>
</Sheet>
<div className={cn("flex min-w-0 flex-1 flex-col", fill && "min-h-0")}>
<header className="flex h-14 shrink-0 items-center gap-2 px-3 sm:px-5">
<button type="button" aria-label="Open menu" onClick={() => setDrawer(true)} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-9 items-center justify-center rounded-xl outline-none focus-visible:ring-[3px] lg:hidden">
<Menu className="size-5" aria-hidden="true" />
</button>
<h1 className="min-w-0 truncate text-[15px] font-medium">{title}</h1>
<div className="ms-auto flex items-center gap-1.5">{actions}</div>
</header>
<div className={cn("flex-1", fill && "flex min-h-0")}>{children}</div>
</div>
</div>
)
}
const buttonBase = "focus-visible:ring-ring/50 inline-flex h-9 shrink-0 items-center justify-center gap-2 rounded-xl px-3.5 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4"
/** Class names for Muse buttons, so links and buttons match. */
const museButton = {
primary: cn(buttonBase, "bg-primary text-primary-foreground hover:opacity-90"),
outline: cn(buttonBase, "bg-card hover:bg-accent border"),
ghost: cn(buttonBase, "hover:bg-accent"),
danger: cn(buttonBase, "bg-card text-destructive border-destructive/40 hover:bg-destructive/10 border"),
}
export { MuseMark, MuseShell, defaultHrefs as museDefaultHrefs, museButton, type MuseHrefs, type MusePage, type MuseShellProps }// Ballmac UI: Muse chat page. https://ui.ballmac.com/templates/template-muse
"use client"
import * as React from "react"
import { FileText, Share2, Sparkles } from "lucide-react"
import { Chat, ChatFooter, ChatMessages } from "@/components/ballmac/ai-chat"
import { Message, MessageActions, MessageAvatar, MessageContent, MessageCopyAction } from "@/components/ballmac/ai-message"
import { ArtifactPanel } from "@/components/ballmac/artifact-panel"
import { Citation } from "@/components/ballmac/citation"
import { ModelPicker } from "@/components/ballmac/model-picker"
import { PromptInput, PromptInputSubmit, PromptInputTextarea, PromptInputToolbar } from "@/components/ballmac/prompt-input"
import { ReasoningDisclosure } from "@/components/ballmac/reasoning-disclosure"
import { StreamingText } from "@/components/ballmac/streaming-text"
import { SuggestionChips } from "@/components/ballmac/suggestion-chips"
import { ThinkingIndicator } from "@/components/ballmac/thinking-indicator"
import { artifacts, models, replies, suggestions } from "@/components/ballmac/templates/muse/muse-data"
import { MuseMark, MuseShell, museButton, type MuseHrefs } from "@/components/ballmac/templates/muse/muse-theme"
import { cn } from "@/lib/utils"
type Turn = { id: string; role: "user" | "assistant"; text: string; reasoning?: string; sources?: boolean; artifact?: boolean; state?: "thinking" | "streaming" | "done" }
const plan = artifacts[0]
const starter: Turn[] = [
{ id: "t1", role: "user", text: "Help me turn these notes into a Q4 plan for the design team. We have an onboarding rebuild, a component library refresh and not enough research." },
{
id: "t2",
role: "assistant",
state: "done",
reasoning: "The notes name three efforts but not owners or dates. A plan needs a few clear bets, a rough order and the risks. I should keep it short, and ask for the owners rather than inventing them.",
text: "Here is a plan that keeps the team to three bets, in the order I would run them.\n\nOnboarding comes first because it has a hard date and the most customer impact. The component library refresh can run alongside it, as long as it stays small and ships in pieces. Research is the one most likely to be squeezed, so I gave it a standing weekly slot instead of a project.\n\nI left owners blank where your notes did not say. I have put the plan in a document so you can edit it.",
sources: true,
artifact: true,
},
{ id: "t3", role: "user", text: "Great. Can you make the risks section sharper?" },
{
id: "t4",
role: "assistant",
state: "done",
text: "Yes. I would name the two risks that can actually derail the quarter and drop the rest: scope creep on onboarding, and engineering capacity in November. Each gets an owner and an early warning sign, so you know in week three, not week eleven.",
},
]
const sourceList = [
{ title: "Planning a quarter with three bets", url: "https://example.com/three-bets", site: "Design Ops Handbook", snippet: "Teams that limit themselves to three bets ship more of them." },
{ title: "Why research gets cut first", url: "https://example.org/research-squeezed", site: "Field Notes", snippet: "A standing weekly slot protects research better than a project plan does." },
]
type MuseChatProps = React.ComponentProps<"div"> & {
/** "conversation" starts with a finished chat and an open document; "empty" starts a new chat with suggestions. */
start?: "conversation" | "empty"
hrefs?: Partial<MuseHrefs>
}
/** The Muse chat: a calm reading column with reasoning, sources and a document panel, plus a model picker and a streamed reply to anything you send. */
function MuseChat({ start = "conversation", hrefs, ...props }: MuseChatProps) {
const empty = start === "empty"
const [turns, setTurns] = React.useState<Turn[]>(empty ? [] : starter)
const [model, setModel] = React.useState("muse-5")
const [panel, setPanel] = React.useState(!empty)
const timers = React.useRef<ReturnType<typeof setTimeout>[]>([])
const count = React.useRef(0)
React.useEffect(() => () => timers.current.forEach(clearTimeout), [])
const busy = turns.some((t) => t.state === "thinking" || t.state === "streaming")
function send(text: string) {
if (busy || !text.trim()) return
const n = turns.length
const reply = replies[count.current++ % replies.length]
setTurns((all) => [...all, { id: `u${n}`, role: "user", text }, { id: `a${n}`, role: "assistant", text: "", state: "thinking" }])
timers.current.push(setTimeout(() => setTurns((all) => all.map((t) => (t.id === `a${n}` ? { ...t, text: reply, state: "streaming" } : t))), 900))
}
function finish(id: string) {
setTurns((all) => all.map((t) => (t.id === id ? { ...t, state: "done" } : t)))
}
const greeting = (
<div className="mx-auto flex max-w-xl flex-col items-center px-4 pt-[10dvh] text-center">
<MuseMark className="size-12" />
<h2 className="mt-6 text-3xl font-medium tracking-[-0.02em] [font-family:var(--muse-serif),ui-serif,Georgia,serif] sm:text-4xl">Good morning, Mina.</h2>
<p className="text-muted-foreground mt-3 text-pretty">What would you like to think through today?</p>
</div>
)
return (
<MuseShell
page={empty ? "new" : "chat"}
activeChat={empty ? undefined : "c1"}
title={empty ? "New chat" : "Q4 plan for the design team"}
hrefs={hrefs}
fill
actions={
<>
<ModelPicker models={models} value={model} onValueChange={setModel} className="max-w-44" />
{!empty && <button type="button" className={cn(museButton.outline, "hidden sm:inline-flex")}><Share2 aria-hidden="true" /> Share</button>}
</>
}
{...props}
>
<div className="relative flex min-h-0 min-w-0 flex-1">
<Chat className="min-w-0 flex-1">
<ChatMessages contentClassName="mx-auto w-full max-w-2xl px-4 py-6">
{turns.length === 0 && greeting}
<div className="space-y-8">
{turns.map((t) =>
t.role === "user" ? (
<Message key={t.id} role="user"><MessageContent className="bg-secondary rounded-2xl px-4 py-3 text-[15px] text-pretty">{t.text}</MessageContent></Message>
) : (
<Message key={t.id} role="assistant">
<MessageAvatar className="bg-transparent"><MuseMark className="size-7" /></MessageAvatar>
<MessageContent className="min-w-0 space-y-4">
{t.state === "thinking" && <ThinkingIndicator variant="dots" label="Thinking" />}
{t.reasoning && <ReasoningDisclosure duration={9}><p className="text-sm text-pretty">{t.reasoning}</p></ReasoningDisclosure>}
{t.text && (
<StreamingText
text={t.text}
animate={t.state === "streaming"}
streaming={t.state === "streaming"}
speed={170}
onAnimationComplete={() => finish(t.id)}
className="text-[17px] leading-8 text-pretty [font-family:var(--muse-serif),ui-serif,Georgia,serif]"
/>
)}
{t.sources && (
<p className="text-muted-foreground text-sm">
Drawing on <Citation sources={sourceList} variant="pill" /> two sources about planning with limited bets.
</p>
)}
{t.artifact && (
<button type="button" onClick={() => setPanel(true)} className="bg-card hover:bg-accent/50 focus-visible:ring-ring/50 flex w-full max-w-sm items-center gap-3 rounded-2xl border p-3 text-start outline-none transition-colors focus-visible:ring-[3px]">
<span aria-hidden="true" className="bg-chart-1/15 text-chart-1 flex size-10 items-center justify-center rounded-xl"><FileText className="size-5" /></span>
<span className="min-w-0 flex-1"><span className="block text-sm font-medium">{plan.title}</span><span className="text-muted-foreground block text-xs">Document · open to edit</span></span>
</button>
)}
</MessageContent>
{t.state === "done" && t.text && <MessageActions><MessageCopyAction value={t.text} /></MessageActions>}
</Message>
)
)}
</div>
</ChatMessages>
<ChatFooter contentClassName="mx-auto w-full max-w-2xl px-4 pb-5">
{turns.length === 0 && <SuggestionChips className="mb-4" variant="cards" label="Try one of these" suggestions={suggestions} onSelect={(p) => send(p)} />}
<PromptInput onSubmit={(v) => send(v)} status={busy ? "streaming" : "idle"} className="rounded-2xl">
<PromptInputTextarea placeholder="Message Muse" aria-label="Message Muse" />
<PromptInputToolbar>
<span className="text-muted-foreground flex items-center gap-1.5 text-xs"><Sparkles className="size-3.5" aria-hidden="true" />{models.find((m) => m.id === model)?.name}</span>
<PromptInputSubmit className="ms-auto" />
</PromptInputToolbar>
</PromptInput>
<p className="text-muted-foreground mt-2 text-center text-xs">Muse can make mistakes. Check anything that matters.</p>
</ChatFooter>
</Chat>
{panel && !empty && (
<div className="bg-background absolute inset-0 z-10 flex xl:static xl:z-auto xl:w-[26rem] xl:shrink-0 xl:border-s 2xl:w-[32rem]">
<ArtifactPanel title={plan.title} kind="Document" icon={<FileText />} code={plan.body} filename={plan.filename} language={plan.language} onClose={() => setPanel(false)} className="h-full w-full rounded-none border-0">
<div className="space-y-5 p-6 [font-family:var(--muse-serif),ui-serif,Georgia,serif]">
<h2 className="text-2xl font-semibold tracking-tight">Q4 design plan</h2>
<section><h3 className="text-chart-1 text-sm font-semibold tracking-wide uppercase [font-family:var(--muse-sans)]">Goals</h3><ul className="mt-2 list-disc space-y-1.5 ps-5 text-[16px] leading-7"><li>Ship the new onboarding flow by Oct 31</li><li>Cut design-to-dev handoff time in half</li><li>Run two customer research rounds</li></ul></section>
<section><h3 className="text-chart-1 text-sm font-semibold tracking-wide uppercase [font-family:var(--muse-sans)]">Three bets</h3><ol className="mt-2 list-decimal space-y-1.5 ps-5 text-[16px] leading-7"><li>Onboarding rebuild</li><li>Component library v2</li><li>Research ops</li></ol></section>
<section><h3 className="text-chart-1 text-sm font-semibold tracking-wide uppercase [font-family:var(--muse-sans)]">Risks and owners</h3><ul className="mt-2 list-disc space-y-1.5 ps-5 text-[16px] leading-7"><li>Scope creep on onboarding (Priya)</li><li>Engineering capacity in November (Dev)</li></ul></section>
</div>
</ArtifactPanel>
</div>
)}
</div>
</MuseShell>
)
}
export { MuseChat, type MuseChatProps }// Ballmac UI: Muse projects page. https://ui.ballmac.com/templates/template-muse
"use client"
import * as React from "react"
import { Check, FileText, MessageSquare, Plus, Upload } from "lucide-react"
import { projects as seed, type MuseProject } from "@/components/ballmac/templates/muse/muse-data"
import { MuseShell, museButton, type MuseHrefs } from "@/components/ballmac/templates/muse/muse-theme"
import { cn } from "@/lib/utils"
const tones = ["bg-chart-1/20", "bg-chart-2/20", "bg-chart-3/25", "bg-chart-4/20", "bg-chart-5/20"]
type MuseProjectsProps = React.ComponentProps<"div"> & { hrefs?: Partial<MuseHrefs> }
/** The Muse projects page: a list on the left, and the selected project with editable instructions, files and chats on the right. */
function MuseProjects({ hrefs, ...props }: MuseProjectsProps) {
const [list, setList] = React.useState<MuseProject[]>(seed)
const [selectedId, setSelectedId] = React.useState(seed[0].id)
const [drafts, setDrafts] = React.useState<Record<string, string>>({})
const [savedId, setSavedId] = React.useState<string | null>(null)
const sel = list.find((p) => p.id === selectedId) ?? list[0]
const draft = drafts[sel.id] ?? sel.instructions
const dirty = draft !== sel.instructions
function save() {
setList((all) => all.map((p) => (p.id === sel.id ? { ...p, instructions: draft } : p)))
setSavedId(sel.id)
}
function addFile() {
setList((all) => all.map((p) => (p.id === sel.id ? { ...p, files: [...p.files, { name: `Upload ${p.files.length + 1}.pdf`, size: "1.2 MB" }] } : p)))
}
function create() {
const id = `new-${list.length}`
setList((all) => [...all, { id, name: `Untitled project ${all.length - seed.length + 1}`, description: "A fresh project. Add instructions and files.", tone: (all.length % 4) + 1, chats: 0, files: [], instructions: "" }])
setSelectedId(id)
}
return (
<MuseShell page="projects" title="Projects" hrefs={hrefs} actions={<button type="button" className={museButton.primary} onClick={create}><Plus aria-hidden="true" /> New project</button>} {...props}>
<main className="mx-auto grid max-w-5xl gap-6 px-4 pb-12 sm:px-6 lg:grid-cols-[18rem_minmax(0,1fr)]">
<section aria-label="All projects">
<ul className="space-y-2">
{list.map((p) => {
const on = p.id === sel.id
return (
<li key={p.id}>
<button type="button" aria-pressed={on} onClick={() => setSelectedId(p.id)} className={cn("focus-visible:ring-ring/50 flex w-full items-center gap-3 rounded-2xl border p-3 text-start outline-none transition-colors focus-visible:ring-[3px]", on ? "bg-card border-foreground/25 shadow-sm" : "hover:bg-accent/60 border-transparent")}>
<span className={cn("flex size-11 shrink-0 items-center justify-center rounded-xl text-base font-semibold", tones[(p.tone - 1) % tones.length])} aria-hidden="true">{p.name[0]}</span>
<span className="min-w-0"><span className="block truncate text-sm font-medium">{p.name}</span><span className="text-muted-foreground block truncate text-xs">{p.chats} chats · {p.files.length} {p.files.length === 1 ? "file" : "files"}</span></span>
</button>
</li>
)
})}
</ul>
</section>
<section aria-labelledby="mp-title" className="min-w-0">
<div className="bg-card rounded-3xl border p-6 sm:p-8">
<div className="flex items-start gap-4">
<span className={cn("flex size-14 shrink-0 items-center justify-center rounded-2xl text-xl font-semibold", tones[(sel.tone - 1) % tones.length])} aria-hidden="true">{sel.name[0]}</span>
<div className="min-w-0"><h2 id="mp-title" className="text-2xl font-medium tracking-[-0.02em] [font-family:var(--muse-serif),ui-serif,Georgia,serif]">{sel.name}</h2><p className="text-muted-foreground mt-1 text-pretty">{sel.description}</p></div>
</div>
<div className="mt-8">
<label htmlFor="mp-inst" className="text-sm font-medium">Instructions</label>
<p className="text-muted-foreground mt-0.5 text-sm">Muse follows these in every chat in this project.</p>
<textarea id="mp-inst" rows={4} value={draft} onChange={(e) => { setDrafts((d) => ({ ...d, [sel.id]: e.target.value })); setSavedId(null) }} placeholder="Tell Muse how to work on this project" className="bg-background focus-visible:ring-ring/50 placeholder:text-muted-foreground mt-3 w-full resize-y rounded-2xl border p-4 text-[15px] leading-relaxed outline-none focus-visible:ring-[3px]" />
<div className="mt-3 flex items-center justify-between gap-3">
<p className="text-muted-foreground flex items-center gap-1.5 text-sm" role="status">{savedId === sel.id ? <><Check className="text-chart-2 size-4" aria-hidden="true" />Saved</> : dirty ? "Unsaved changes" : ""}</p>
<button type="button" onClick={save} disabled={!dirty} className={museButton.primary}>Save instructions</button>
</div>
</div>
<div className="mt-8">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium">Files <span className="text-muted-foreground font-normal">({sel.files.length})</span></h3>
<button type="button" onClick={addFile} className={museButton.outline}><Upload aria-hidden="true" /> Add file</button>
</div>
{sel.files.length === 0 ? (
<p className="text-muted-foreground mt-3 rounded-2xl border border-dashed p-6 text-center text-sm">No files yet. Add documents Muse should always have on hand.</p>
) : (
<ul className="mt-3 divide-y rounded-2xl border">
{sel.files.map((f) => <li key={f.name} className="flex items-center gap-3 px-4 py-3 text-sm"><FileText className="text-muted-foreground size-4" aria-hidden="true" /><span className="min-w-0 flex-1 truncate font-medium">{f.name}</span><span className="text-muted-foreground text-xs">{f.size}</span></li>)}
</ul>
)}
</div>
<div className="mt-8">
<h3 className="text-sm font-medium">Chats in this project</h3>
<ul className="mt-3 grid gap-2 sm:grid-cols-2">
{["Weekly update draft", "Planning notes", "Feedback summary"].slice(0, Math.min(3, Math.max(1, sel.chats))).map((c) => (
<li key={c}><a href={hrefs?.chat ?? "/muse"} className="hover:bg-accent focus-visible:ring-ring/50 flex items-center gap-2.5 rounded-xl border p-3 text-sm outline-none focus-visible:ring-[3px]"><MessageSquare className="text-muted-foreground size-4" aria-hidden="true" />{c}</a></li>
))}
</ul>
{sel.chats === 0 && <p className="text-muted-foreground mt-2 text-sm">No chats yet. Start one from the sidebar.</p>}
</div>
</div>
</section>
</main>
</MuseShell>
)
}
export { MuseProjects, type MuseProjectsProps }// Ballmac UI: Muse library page. https://ui.ballmac.com/templates/template-muse
"use client"
import * as React from "react"
import { Database, FileCode2, FileText, Search } from "lucide-react"
import { CopyButton } from "@/components/ballmac/copy-button"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ballmac/dialog"
import { artifacts, type MuseArtifact } from "@/components/ballmac/templates/muse/muse-data"
import { MuseShell, type MuseHrefs } from "@/components/ballmac/templates/muse/muse-theme"
import { cn } from "@/lib/utils"
type Filter = "All" | MuseArtifact["kind"]
const filters: Filter[] = ["All", "Document", "Code", "Data"]
const icons = { Document: FileText, Code: FileCode2, Data: Database }
type MuseLibraryProps = React.ComponentProps<"div"> & { hrefs?: Partial<MuseHrefs> }
/** The Muse library: everything Muse has written for you, filterable by type, with a preview you can copy from. */
function MuseLibrary({ hrefs, ...props }: MuseLibraryProps) {
const [filter, setFilter] = React.useState<Filter>("All")
const [query, setQuery] = React.useState("")
const [openId, setOpenId] = React.useState<string | null>(null)
const q = query.trim().toLowerCase()
const shown = artifacts.filter((a) => (filter === "All" || a.kind === filter) && (!q || a.title.toLowerCase().includes(q)))
const open = artifacts.find((a) => a.id === openId)
return (
<MuseShell page="library" title="Library" hrefs={hrefs} {...props}>
<main className="mx-auto max-w-5xl px-4 pb-12 sm:px-6">
<p className="text-muted-foreground max-w-xl text-pretty">Documents, code and data Muse has made for you, kept together so you can find them again.</p>
<div className="mt-6 flex flex-wrap items-center gap-3">
<div role="group" aria-label="Filter by type" className="flex gap-1.5">
{filters.map((f) => (
<button key={f} type="button" aria-pressed={filter === f} onClick={() => setFilter(f)} className={cn("focus-visible:ring-ring/50 h-9 rounded-full border px-4 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px]", filter === f ? "bg-primary text-primary-foreground border-transparent" : "bg-card hover:bg-accent")}>
{f}
</button>
))}
</div>
<div className="relative ms-auto w-full sm:w-64">
<Search className="text-muted-foreground pointer-events-none absolute top-1/2 start-3.5 size-4 -translate-y-1/2" aria-hidden="true" />
<input type="search" aria-label="Search the library" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search" className="bg-card focus-visible:ring-ring/50 placeholder:text-muted-foreground h-9 w-full rounded-full border pe-4 ps-10 text-sm outline-none focus-visible:ring-[3px]" />
</div>
</div>
<p className="sr-only" role="status">{shown.length} items</p>
{shown.length === 0 ? (
<div className="bg-card text-muted-foreground mt-8 rounded-3xl border border-dashed px-6 py-20 text-center text-sm">Nothing here yet. Ask Muse to write something and it will land in your library.</div>
) : (
<ul className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{shown.map((a) => {
const Icon = icons[a.kind]
return (
<li key={a.id}>
<button type="button" onClick={() => setOpenId(a.id)} className="bg-card hover:border-foreground/25 focus-visible:ring-ring/50 group flex h-full w-full flex-col overflow-hidden rounded-3xl border text-start outline-none transition-all hover:shadow-md focus-visible:ring-[3px]">
<div aria-hidden="true" className={cn("bg-surface h-36 overflow-hidden border-b p-5 text-[11px] leading-5", a.kind === "Code" || a.kind === "Data" ? "font-mono" : "[font-family:var(--muse-serif),ui-serif,Georgia,serif] text-[13px]")}>
{a.lines.map((l, i) => <p key={i} className={cn("truncate", i === 0 && a.kind === "Document" ? "text-foreground font-semibold" : "text-muted-foreground")}>{l}</p>)}
</div>
<div className="flex flex-1 items-center gap-3 p-4">
<span className="bg-chart-1/12 text-chart-1 flex size-9 shrink-0 items-center justify-center rounded-xl"><Icon className="size-4" aria-hidden="true" /></span>
<span className="min-w-0"><span className="block truncate text-sm font-medium">{a.title}</span><span className="text-muted-foreground block text-xs">{a.kind} · {a.updated}</span></span>
</div>
</button>
</li>
)
})}
</ul>
)}
</main>
<Dialog open={!!open} onOpenChange={(o) => !o && setOpenId(null)}>
<DialogContent className="max-w-2xl">
{open && (
<>
<DialogHeader>
<DialogTitle>{open.title}</DialogTitle>
<DialogDescription>{open.kind} · {open.filename} · updated {open.updated.toLowerCase()}</DialogDescription>
</DialogHeader>
<div className="relative">
<pre tabIndex={0} aria-label={`${open.title} contents`} className={cn("bg-surface focus-visible:ring-ring/50 max-h-[50dvh] overflow-auto rounded-2xl border p-5 text-sm leading-7 whitespace-pre-wrap outline-none focus-visible:ring-[3px]", open.kind === "Document" ? "[font-family:var(--muse-serif),ui-serif,Georgia,serif] text-[15px]" : "font-mono text-[13px] leading-6")}>{open.body}</pre>
<CopyButton value={open.body} ariaLabel="Copy contents" className="absolute top-2 end-2" />
</div>
</>
)}
</DialogContent>
</Dialog>
</MuseShell>
)
}
export { MuseLibrary, type MuseLibraryProps }// Ballmac UI: Muse settings page. https://ui.ballmac.com/templates/template-muse
"use client"
import * as React from "react"
import { Trash2 } from "lucide-react"
import { Slider } from "@/components/ballmac/slider"
import { SegmentedControl, SegmentedControlItem } from "@/components/ballmac/segmented-control"
import { Switch } from "@/components/ballmac/switch"
import { memories as seedMemories, models } from "@/components/ballmac/templates/muse/muse-data"
import { MuseShell, museButton, type MuseHrefs } from "@/components/ballmac/templates/muse/muse-theme"
import { cn } from "@/lib/utils"
type Font = "serif" | "sans"
type MuseSettingsProps = React.ComponentProps<"div"> & { hrefs?: Partial<MuseHrefs> }
function Card({ id, title, description, children }: { id: string; title: string; description: string; children: React.ReactNode }) {
return (
<section aria-labelledby={id} className="bg-card rounded-3xl border p-6 sm:p-7">
<h2 id={id} className="text-lg font-medium">{title}</h2>
<p className="text-muted-foreground mt-1 text-sm text-pretty">{description}</p>
<div className="mt-5">{children}</div>
</section>
)
}
/** The Muse settings page: reading preferences with a live preview, default model, memory you can edit, and data controls with a type-to-confirm delete. */
function MuseSettings({ hrefs, ...props }: MuseSettingsProps) {
const [font, setFont] = React.useState<Font>("serif")
const [size, setSize] = React.useState(17)
const [model, setModel] = React.useState("muse-5")
const [memoryOn, setMemoryOn] = React.useState(true)
const [memories, setMemories] = React.useState(seedMemories)
const [confirm, setConfirm] = React.useState("")
const [deleted, setDeleted] = React.useState(false)
return (
<MuseShell page="settings" title="Settings" hrefs={hrefs} {...props}>
<main className="mx-auto max-w-2xl space-y-5 px-4 pb-14 sm:px-6">
<Card id="ms-reading" title="Reading" description="Choose how replies look. The preview updates as you change it.">
<div className="flex flex-wrap items-center justify-between gap-4">
<p id="ms-font-label" className="text-sm font-medium">Reply font</p>
<SegmentedControl aria-labelledby="ms-font-label" value={font} onValueChange={(v) => setFont(v as Font)}>
<SegmentedControlItem value="serif">Serif</SegmentedControlItem>
<SegmentedControlItem value="sans">Sans</SegmentedControlItem>
</SegmentedControl>
</div>
<div className="mt-6">
<div className="flex items-baseline justify-between text-sm"><label id="ms-size-label" className="font-medium">Text size</label><output className="text-muted-foreground tabular-nums">{size}px</output></div>
<Slider className="mt-3" aria-labelledby="ms-size-label" min={15} max={21} step={1} value={[size]} onValueChange={(v) => setSize(v[0] ?? size)} formatValue={(v) => `${v} pixels`} />
</div>
<p className={cn("bg-surface mt-6 rounded-2xl border p-5 text-pretty", font === "serif" ? "[font-family:var(--muse-serif),ui-serif,Georgia,serif]" : "")} style={{ fontSize: size, lineHeight: 1.75 }}>
An index fund is a basket that owns a small slice of hundreds of companies, so you are not betting on any one of them.
</p>
</Card>
<Card id="ms-model" title="Default model" description="New chats start with this model. You can change it in any chat.">
<div role="radiogroup" aria-label="Default model" className="grid gap-2">
{models.map((m) => (
<label key={m.id} className={cn("has-[:focus-visible]:ring-ring/50 flex cursor-pointer items-start gap-3 rounded-2xl border p-4 transition-colors has-[:focus-visible]:ring-[3px]", model === m.id ? "border-foreground/40 bg-accent/60" : "hover:bg-accent/40")}>
<input type="radio" name="default-model" value={m.id} checked={model === m.id} onChange={() => setModel(m.id)} className="accent-chart-1 mt-1 size-4" />
<span><span className="block text-sm font-medium">{m.name}</span><span className="text-muted-foreground block text-sm text-pretty">{m.description}</span></span>
</label>
))}
</div>
</Card>
<Card id="ms-memory" title="Memory" description="Muse remembers useful things between chats. You decide what stays.">
<div className="flex items-center justify-between gap-4">
<p id="ms-mem-label" className="text-sm font-medium">Remember things about me</p>
<Switch aria-labelledby="ms-mem-label" checked={memoryOn} onCheckedChange={setMemoryOn} />
</div>
{memoryOn && (
<>
<ul className="mt-4 divide-y rounded-2xl border">
{memories.map((m) => (
<li key={m} className="flex items-center gap-3 px-4 py-3 text-sm">
<span className="min-w-0 flex-1 text-pretty">{m}</span>
<button type="button" aria-label={`Forget: ${m}`} onClick={() => setMemories((all) => all.filter((x) => x !== m))} className="text-muted-foreground hover:text-destructive hover:bg-accent focus-visible:ring-ring/50 inline-flex size-8 shrink-0 items-center justify-center rounded-lg outline-none focus-visible:ring-[3px]"><Trash2 className="size-4" aria-hidden="true" /></button>
</li>
))}
{memories.length === 0 && <li className="text-muted-foreground px-4 py-8 text-center text-sm">Muse hasn’t remembered anything yet.</li>}
</ul>
<p className="text-muted-foreground mt-2 text-xs" role="status">{memories.length} {memories.length === 1 ? "memory" : "memories"} saved</p>
</>
)}
</Card>
<Card id="ms-data" title="Your data" description="Export everything, or delete your account and all chats.">
<button type="button" className={museButton.outline}>Export my data</button>
<div className="border-destructive/40 mt-6 rounded-2xl border p-5">
<h3 className="text-sm font-medium">Delete account</h3>
<p className="text-muted-foreground mt-1 text-sm text-pretty">This removes all chats, projects and your library. It can’t be undone.</p>
{deleted ? (
<p role="status" className="text-destructive mt-4 text-sm font-medium">Your account is scheduled for deletion in 7 days.</p>
) : (
<>
<label htmlFor="ms-confirm" className="mt-4 block text-sm">Type <span className="font-mono font-semibold">delete my account</span> to confirm</label>
<input id="ms-confirm" value={confirm} onChange={(e) => setConfirm(e.target.value)} className="bg-background focus-visible:ring-ring/50 mt-2 h-10 w-full rounded-xl border px-3 text-sm outline-none focus-visible:ring-[3px]" />
<button type="button" disabled={confirm !== "delete my account"} onClick={() => setDeleted(true)} className={cn(museButton.danger, "mt-3")}>Delete account</button>
</>
)}
</div>
</Card>
</main>
</MuseShell>
)
}
export { MuseSettings, type MuseSettingsProps }// Ballmac UI: Muse template route. https://ui.ballmac.com/templates/template-muse
import type { Metadata } from "next"
import { MuseChat } from "@/components/ballmac/templates/muse/muse-chat"
export const metadata: Metadata = {
title: "Muse",
description: "A calm place to think with an AI.",
}
export default function Page() {
return <MuseChat start="conversation" />
}// Ballmac UI: Muse template route. https://ui.ballmac.com/templates/template-muse
import type { Metadata } from "next"
import { MuseChat } from "@/components/ballmac/templates/muse/muse-chat"
export const metadata: Metadata = {
title: "New chat · Muse",
description: "Start a new conversation.",
}
export default function Page() {
return <MuseChat start="empty" />
}// Ballmac UI: Muse template route. https://ui.ballmac.com/templates/template-muse
import type { Metadata } from "next"
import { MuseProjects } from "@/components/ballmac/templates/muse/muse-projects"
export const metadata: Metadata = {
title: "Projects · Muse",
description: "Instructions, files and chats for each project.",
}
export default function Page() {
return <MuseProjects />
}// Ballmac UI: Muse template route. https://ui.ballmac.com/templates/template-muse
import type { Metadata } from "next"
import { MuseLibrary } from "@/components/ballmac/templates/muse/muse-library"
export const metadata: Metadata = {
title: "Library · Muse",
description: "Everything Muse has made for you.",
}
export default function Page() {
return <MuseLibrary />
}// Ballmac UI: Muse template route. https://ui.ballmac.com/templates/template-muse
import type { Metadata } from "next"
import { MuseSettings } from "@/components/ballmac/templates/muse/muse-settings"
export const metadata: Metadata = {
title: "Settings · Muse",
description: "Reading, models, memory and data.",
}
export default function Page() {
return <MuseSettings />
}Installation
$ pnpm dlx shadcn@latest add @ballmac/template-museInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/ai-chat @ballmac/ai-message @ballmac/artifact-panel @ballmac/citation @ballmac/copy-button @ballmac/dialog @ballmac/model-picker @ballmac/prompt-input @ballmac/reasoning-disclosure @ballmac/segmented-control @ballmac/sheet @ballmac/slider @ballmac/streaming-text @ballmac/suggestion-chips @ballmac/switch @ballmac/thinking-indicatorCopy the source into your project.
components/ballmac/templates/muse/muse-fonts.ts// Ballmac UI: Muse template fonts. https://ui.ballmac.com/templates/template-muse import { DM_Sans, Source_Serif_4 } from "next/font/google" export const museSans = DM_Sans({ variable: "--muse-sans", subsets: ["latin"], display: "swap", preload: false }) /** The reading face for replies: a book serif that stays comfortable for long answers. */ export const museSerif = Source_Serif_4({ variable: "--muse-serif", subsets: ["latin"], display: "swap", preload: false })components/ballmac/templates/muse/muse-data.ts// Ballmac UI: Muse sample data. https://ui.ballmac.com/templates/template-muse import type { ModelOption } from "@/components/ballmac/model-picker" export const models: ModelOption[] = [ { id: "muse-5", name: "Muse 5", provider: "Muse", description: "The most capable model. Best for long, careful work.", capabilities: ["vision", "reasoning", "tools"], contextWindow: 500000, cost: 3, isNew: true }, { id: "muse-5-fast", name: "Muse 5 Fast", provider: "Muse", description: "Quick answers for everyday questions.", capabilities: ["vision", "tools"], contextWindow: 200000, cost: 1 }, { id: "muse-4-writer", name: "Muse 4 Writer", provider: "Muse", description: "Tuned for drafting, editing and tone.", capabilities: ["reasoning"], contextWindow: 200000, cost: 2 }, ] export type MuseChatItem = { id: string; title: string; group: "Today" | "Yesterday" | "Previous 7 days"; project?: string } export const history: MuseChatItem[] = [ { id: "c1", title: "Q4 plan for the design team", group: "Today", project: "Design team" }, { id: "c2", title: "Rename the onboarding emails", group: "Today" }, { id: "c3", title: "Explain index funds like I’m ten", group: "Yesterday" }, { id: "c4", title: "Recipe: weeknight miso aubergine", group: "Yesterday" }, { id: "c5", title: "Review my cover letter", group: "Previous 7 days" }, { id: "c6", title: "Trip to Lisbon in March", group: "Previous 7 days", project: "Travel" }, { id: "c7", title: "SQL: cohort retention query", group: "Previous 7 days", project: "Analytics" }, ] export type MuseProject = { id: string; name: string; description: string; tone: number; chats: number; files: { name: string; size: string }[]; instructions: string } export const projects: MuseProject[] = [ { id: "design", name: "Design team", description: "Roadmaps, critique notes and the weekly update.", tone: 1, chats: 14, files: [{ name: "Q3 retro.md", size: "12 KB" }, { name: "Brand guidelines.pdf", size: "2.1 MB" }, { name: "Research summary.docx", size: "88 KB" }], instructions: "Write in plain, warm language. Keep updates under 200 words. Always end with the next step and who owns it." }, { id: "travel", name: "Travel", description: "Itineraries, bookings and packing lists.", tone: 2, chats: 6, files: [{ name: "Lisbon.pdf", size: "640 KB" }, { name: "Packing list.md", size: "3 KB" }], instructions: "Prefer walkable neighbourhoods and small restaurants. Give times in 24-hour format." }, { id: "analytics", name: "Analytics", description: "SQL, dashboards and metric definitions.", tone: 3, chats: 22, files: [{ name: "Schema.sql", size: "34 KB" }, { name: "Metric glossary.md", size: "9 KB" }, { name: "Cohorts.csv", size: "1.4 MB" }, { name: "Notes.txt", size: "2 KB" }], instructions: "Use Postgres syntax. Explain each query in two sentences before showing it." }, { id: "writing", name: "Writing", description: "Essays, drafts and edits in progress.", tone: 4, chats: 9, files: [{ name: "Essay draft.md", size: "28 KB" }], instructions: "Be a candid editor. Point out the weakest paragraph first." }, ] export type MuseArtifact = { id: string; title: string; kind: "Document" | "Code" | "Data"; updated: string; lines: string[]; body: string; filename: string; language: string } export const artifacts: MuseArtifact[] = [ { id: "a1", title: "Q4 design plan", kind: "Document", updated: "Today", lines: ["Goals", "Three bets", "Risks and owners"], filename: "q4-design-plan.md", language: "text", body: "# Q4 design plan\n\n## Goals\n- Ship the new onboarding flow by Oct 31\n- Cut design-to-dev handoff time in half\n- Run two customer research rounds\n\n## Three bets\n1. Onboarding rebuild\n2. Component library v2\n3. Research ops\n\n## Risks and owners\n- Scope creep on onboarding (Priya)\n- Engineering capacity in November (Dev)" }, { id: "a2", title: "Cohort retention query", kind: "Code", updated: "3 days ago", lines: ["WITH cohorts AS (", " SELECT user_id", " FROM signups"], filename: "retention.sql", language: "text", body: "WITH cohorts AS (\n SELECT user_id, date_trunc('week', created_at) AS cohort\n FROM signups\n)\nSELECT cohort,\n count(*) AS users,\n count(*) FILTER (WHERE active_week_4) AS retained\nFROM cohorts\nJOIN activity USING (user_id)\nGROUP BY 1\nORDER BY 1;" }, { id: "a3", title: "Lisbon itinerary", kind: "Document", updated: "Last week", lines: ["Day 1: Alfama", "Day 2: Belém", "Day 3: Sintra"], filename: "lisbon.md", language: "text", body: "# Lisbon, 4 days\n\n**Day 1** Alfama by foot, lunch at Taberna da Rua das Flores.\n**Day 2** Belém, pastéis, the Tower.\n**Day 3** Sintra by train, back by 18:00.\n**Day 4** Slow morning, LX Factory." }, { id: "a4", title: "Onboarding email copy", kind: "Document", updated: "Last week", lines: ["Subject: Welcome to Fieldnote", "Hi {{name}},", "Here is how to start"], filename: "emails.md", language: "text", body: "Subject: Welcome to Fieldnote\n\nHi {{name}},\n\nThanks for joining. Here is the one thing to do first: write your first note." }, { id: "a5", title: "Weekly metrics", kind: "Data", updated: "2 weeks ago", lines: ["week,signups,active", "36,412,288", "37,447,301"], filename: "metrics.csv", language: "text", body: "week,signups,active\n36,412,288\n37,447,301\n38,501,336\n39,478,342" }, { id: "a6", title: "Debounce hook", kind: "Code", updated: "2 weeks ago", lines: ["export function useDebounce<T>(", " value: T, delay = 300", ") {"], filename: "use-debounce.ts", language: "typescript", body: "export function useDebounce<T>(value: T, delay = 300) {\n const [debounced, setDebounced] = useState(value)\n useEffect(() => {\n const id = setTimeout(() => setDebounced(value), delay)\n return () => clearTimeout(id)\n }, [value, delay])\n return debounced\n}" }, ] export const memories = [ "Works on the design team at Fieldnote Goods", "Prefers short answers with one clear next step", "Based in Portland, writes in US English", "Vegetarian; cooks on weeknights", "Planning a trip to Lisbon in March", ] export const suggestions = [ { label: "Plan my week", prompt: "Help me plan my week. I have three deadlines and a trip on Friday.", description: "Turn a messy to-do list into a calm schedule" }, { label: "Explain something", prompt: "Explain how index funds work, like I’m ten.", description: "A clear answer with no jargon" }, { label: "Draft an email", prompt: "Draft a kind but firm email asking a client to pay an overdue invoice.", description: "Tone, structure and a subject line" }, { label: "Review my writing", prompt: "Be a candid editor: tell me what is weak in my opening paragraph.", description: "Honest feedback, weakest part first" }, ] /** Scripted replies, cycled so a demo conversation always has something sensible to say. */ export const replies = [ "Here is a calm way to think about it. Start with what is fixed, such as meetings and the Friday trip, then place the deadlines in the gaps with the hardest one first, while your energy is highest.\n\nI would put the biggest deadline on Tuesday morning, leave Thursday light for surprises, and finish anything that can’t slip by Wednesday night. Want me to turn this into a day-by-day list?", "Good question. The short version: an index fund is a basket that owns a tiny slice of hundreds of companies, so you are not betting on any single one.\n\nWhen the companies in the basket do well, so does your slice. Because nobody is picking winners, the fees are very low, which is most of why they work.", "Of course. Here is a version that stays warm while being clear about the date:\n\nSubject: Quick reminder about invoice 1042\n\nHi Sam, I hope the project is going well. A note that invoice 1042 was due on the 15th. Could you let me know when to expect payment? Happy to resend it if that helps.", ]components/ballmac/templates/muse/muse-theme.tsx// Ballmac UI: Muse template shell. https://ui.ballmac.com/templates/template-muse "use client" import * as React from "react" import { Library, MessageSquarePlus, Menu, FolderKanban, Settings } from "lucide-react" import { Sheet, SheetContent, SheetDescription, SheetTitle } from "@/components/ballmac/sheet" import { history } from "@/components/ballmac/templates/muse/muse-data" import { museSans, museSerif } from "@/components/ballmac/templates/muse/muse-fonts" import { cn } from "@/lib/utils" type MusePage = "chat" | "new" | "projects" | "library" | "settings" type MuseHrefs = Record<MusePage, string> const defaultHrefs: MuseHrefs = { chat: "/muse", new: "/muse/new", projects: "/muse/projects", library: "/muse/library", settings: "/muse/settings" } /** Muse's palette: lilac-tinted paper with plum ink and one coral. Calm, roomy and easy on the eyes. */ const museCss = ` .muse-theme,body:not(:has([data-gallery])):has(.muse-theme){--background:oklch(0.978 0.008 300);--foreground:oklch(0.24 0.03 300);--card:oklch(0.995 0.004 300);--card-foreground:oklch(0.24 0.03 300);--popover:oklch(0.995 0.004 300);--popover-foreground:oklch(0.24 0.03 300);--primary:oklch(0.3 0.04 300);--primary-foreground:oklch(0.98 0.006 300);--secondary:oklch(0.95 0.012 300);--secondary-foreground:oklch(0.24 0.03 300);--muted:oklch(0.95 0.012 300);--muted-foreground:oklch(0.48 0.025 300);--accent:oklch(0.935 0.018 300);--accent-foreground:oklch(0.24 0.03 300);--border:oklch(0.24 0.03 300 / 12%);--input:oklch(0.24 0.03 300 / 17%);--ring:oklch(0.55 0.16 28);--surface:oklch(0.962 0.01 300);--destructive:oklch(0.54 0.21 25);--chart-1:oklch(0.52 0.16 28);--chart-2:oklch(0.5 0.12 160);--chart-3:oklch(0.62 0.13 75);--chart-4:oklch(0.5 0.15 300);--chart-5:oklch(0.5 0.1 240);--radius:0.875rem} .dark .muse-theme,.dark body:not(:has([data-gallery])):has(.muse-theme){--background:oklch(0.18 0.014 300);--foreground:oklch(0.95 0.008 300);--card:oklch(0.215 0.016 300);--card-foreground:oklch(0.95 0.008 300);--popover:oklch(0.235 0.018 300);--popover-foreground:oklch(0.95 0.008 300);--primary:oklch(0.95 0.008 300);--primary-foreground:oklch(0.22 0.02 300);--secondary:oklch(0.26 0.018 300);--secondary-foreground:oklch(0.95 0.008 300);--muted:oklch(0.25 0.018 300);--muted-foreground:oklch(0.73 0.02 300);--accent:oklch(0.285 0.02 300);--accent-foreground:oklch(0.95 0.008 300);--border:oklch(1 0 0 / 10%);--input:oklch(1 0 0 / 14%);--ring:oklch(0.74 0.14 30);--surface:oklch(0.2 0.015 300);--destructive:oklch(0.7 0.19 25);--chart-1:oklch(0.76 0.13 30);--chart-2:oklch(0.78 0.13 160);--chart-3:oklch(0.83 0.13 80);--chart-4:oklch(0.75 0.13 300);--chart-5:oklch(0.76 0.1 240)} body:not(:has([data-gallery])):has(.muse-theme){font-family:var(--muse-sans),ui-sans-serif,system-ui,sans-serif} ` const nav: { key: MusePage; label: string; icon: typeof Library }[] = [ { key: "projects", label: "Projects", icon: FolderKanban }, { key: "library", label: "Library", icon: Library }, { key: "settings", label: "Settings", icon: Settings }, ] const groups = ["Today", "Yesterday", "Previous 7 days"] as const function MuseMark({ className }: { className?: string }) { return ( <svg viewBox="0 0 28 28" aria-hidden="true" className={cn("size-7", className)} fill="none"> <circle cx="14" cy="14" r="13" fill="var(--chart-1)" /> <path d="M8 18c1.5-5 3-8 6-8s4.5 3 6 8" stroke="var(--card)" strokeWidth="2.2" strokeLinecap="round" /> <circle cx="14" cy="9" r="1.6" fill="var(--card)" /> </svg> ) } function SidebarBody({ page, hrefs, activeChat, onNavigate }: { page: MusePage; hrefs: MuseHrefs; activeChat?: string; onNavigate?: () => void }) { return ( <div className="flex h-full flex-col"> <div className="flex h-14 items-center gap-2.5 px-4"> <MuseMark /> <span className="text-lg font-semibold tracking-tight">Muse</span> </div> <div className="px-3"> <a href={hrefs.new} onClick={onNavigate} aria-current={page === "new" ? "page" : undefined} className="bg-card hover:bg-accent focus-visible:ring-ring/50 flex items-center gap-2 rounded-xl border px-3 py-2.5 text-sm font-medium shadow-xs outline-none transition-colors focus-visible:ring-[3px]"> <MessageSquarePlus className="text-chart-1 size-4" aria-hidden="true" /> New chat </a> <nav aria-label="Main" className="mt-3 space-y-0.5"> {nav.map((n) => ( <a key={n.key} href={hrefs[n.key]} onClick={onNavigate} aria-current={page === n.key ? "page" : undefined} className="text-muted-foreground hover:text-foreground hover:bg-accent aria-[current=page]:bg-accent aria-[current=page]:text-foreground focus-visible:ring-ring/50 flex items-center gap-2.5 rounded-xl px-3 py-2 text-sm outline-none transition-colors focus-visible:ring-[3px]"> <n.icon className="size-4" aria-hidden="true" /> {n.label} </a> ))} </nav> </div> <div className="mt-4 min-h-0 flex-1 overflow-y-auto px-3 pb-3"> {groups.map((g) => ( <section key={g} aria-label={g} className="mb-4"> <h2 className="text-muted-foreground px-3 pb-1.5 text-xs font-medium">{g}</h2> <ul> {history.filter((h) => h.group === g).map((h) => ( <li key={h.id}> <a href={hrefs.chat} onClick={onNavigate} aria-current={h.id === activeChat ? "page" : undefined} className="text-foreground/85 hover:bg-accent aria-[current=page]:bg-accent focus-visible:ring-ring/50 block truncate rounded-xl px-3 py-2 text-sm outline-none transition-colors focus-visible:ring-[3px]"> {h.title} </a> </li> ))} </ul> </section> ))} </div> <div className="border-t p-3"> <div className="flex items-center gap-2.5 rounded-xl p-2"> <span className="bg-chart-4/25 flex size-8 items-center justify-center rounded-full text-xs font-semibold" aria-hidden="true">MK</span> <span className="min-w-0 flex-1 leading-tight"><span className="block truncate text-sm font-medium">Mina Kovac</span><span className="text-muted-foreground block text-xs">Pro plan</span></span> </div> </div> </div> ) } type MuseShellProps = Omit<React.ComponentProps<"div">, "title"> & { /** The page being shown, so its nav link is marked current. */ page: MusePage /** Heading in the top bar. */ title: React.ReactNode /** The chat to mark current in the history list. */ activeChat?: string /** Override where pages live (used by previews). */ hrefs?: Partial<MuseHrefs> /** Controls on the right of the top bar. */ actions?: React.ReactNode /** Lets the content fill the height of the window (the chat does), instead of scrolling the page. */ fill?: boolean } /** Muse's frame: a history sidebar, a quiet top bar and room to read. Wrap every Muse page in it. */ function MuseShell({ page, title, activeChat, hrefs: overrides, actions, fill, className, style, children, ...props }: MuseShellProps) { const hrefs = { ...defaultHrefs, ...overrides } const [drawer, setDrawer] = React.useState(false) // Portaled UI (menus, dialogs, sheets) lives outside this element, so <body> gets the font variables too. React.useEffect(() => { const classes = [museSans.variable, museSerif.variable].filter(Boolean) document.body.classList.add(...classes) return () => document.body.classList.remove(...classes) }, []) return ( <div data-slot="muse" className={cn("muse-theme bg-background text-foreground flex min-h-dvh", fill && "h-dvh overflow-hidden", museSans.variable, museSerif.variable, className)} style={{ fontFamily: "var(--muse-sans), ui-sans-serif, system-ui, sans-serif", ...style }} {...props} > <style>{museCss}</style> <aside className="bg-surface hidden w-64 shrink-0 border-e lg:block"> <SidebarBody page={page} hrefs={hrefs} activeChat={activeChat} /> </aside> <Sheet open={drawer} onOpenChange={setDrawer}> <SheetContent side="start" className="w-72 p-0" closeLabel="Close menu"> <SheetTitle className="sr-only">Menu</SheetTitle> <SheetDescription className="sr-only">Chats, projects and settings</SheetDescription> <SidebarBody page={page} hrefs={hrefs} activeChat={activeChat} onNavigate={() => setDrawer(false)} /> </SheetContent> </Sheet> <div className={cn("flex min-w-0 flex-1 flex-col", fill && "min-h-0")}> <header className="flex h-14 shrink-0 items-center gap-2 px-3 sm:px-5"> <button type="button" aria-label="Open menu" onClick={() => setDrawer(true)} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-9 items-center justify-center rounded-xl outline-none focus-visible:ring-[3px] lg:hidden"> <Menu className="size-5" aria-hidden="true" /> </button> <h1 className="min-w-0 truncate text-[15px] font-medium">{title}</h1> <div className="ms-auto flex items-center gap-1.5">{actions}</div> </header> <div className={cn("flex-1", fill && "flex min-h-0")}>{children}</div> </div> </div> ) } const buttonBase = "focus-visible:ring-ring/50 inline-flex h-9 shrink-0 items-center justify-center gap-2 rounded-xl px-3.5 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4" /** Class names for Muse buttons, so links and buttons match. */ const museButton = { primary: cn(buttonBase, "bg-primary text-primary-foreground hover:opacity-90"), outline: cn(buttonBase, "bg-card hover:bg-accent border"), ghost: cn(buttonBase, "hover:bg-accent"), danger: cn(buttonBase, "bg-card text-destructive border-destructive/40 hover:bg-destructive/10 border"), } export { MuseMark, MuseShell, defaultHrefs as museDefaultHrefs, museButton, type MuseHrefs, type MusePage, type MuseShellProps }components/ballmac/templates/muse/muse-chat.tsx// Ballmac UI: Muse chat page. https://ui.ballmac.com/templates/template-muse "use client" import * as React from "react" import { FileText, Share2, Sparkles } from "lucide-react" import { Chat, ChatFooter, ChatMessages } from "@/components/ballmac/ai-chat" import { Message, MessageActions, MessageAvatar, MessageContent, MessageCopyAction } from "@/components/ballmac/ai-message" import { ArtifactPanel } from "@/components/ballmac/artifact-panel" import { Citation } from "@/components/ballmac/citation" import { ModelPicker } from "@/components/ballmac/model-picker" import { PromptInput, PromptInputSubmit, PromptInputTextarea, PromptInputToolbar } from "@/components/ballmac/prompt-input" import { ReasoningDisclosure } from "@/components/ballmac/reasoning-disclosure" import { StreamingText } from "@/components/ballmac/streaming-text" import { SuggestionChips } from "@/components/ballmac/suggestion-chips" import { ThinkingIndicator } from "@/components/ballmac/thinking-indicator" import { artifacts, models, replies, suggestions } from "@/components/ballmac/templates/muse/muse-data" import { MuseMark, MuseShell, museButton, type MuseHrefs } from "@/components/ballmac/templates/muse/muse-theme" import { cn } from "@/lib/utils" type Turn = { id: string; role: "user" | "assistant"; text: string; reasoning?: string; sources?: boolean; artifact?: boolean; state?: "thinking" | "streaming" | "done" } const plan = artifacts[0] const starter: Turn[] = [ { id: "t1", role: "user", text: "Help me turn these notes into a Q4 plan for the design team. We have an onboarding rebuild, a component library refresh and not enough research." }, { id: "t2", role: "assistant", state: "done", reasoning: "The notes name three efforts but not owners or dates. A plan needs a few clear bets, a rough order and the risks. I should keep it short, and ask for the owners rather than inventing them.", text: "Here is a plan that keeps the team to three bets, in the order I would run them.\n\nOnboarding comes first because it has a hard date and the most customer impact. The component library refresh can run alongside it, as long as it stays small and ships in pieces. Research is the one most likely to be squeezed, so I gave it a standing weekly slot instead of a project.\n\nI left owners blank where your notes did not say. I have put the plan in a document so you can edit it.", sources: true, artifact: true, }, { id: "t3", role: "user", text: "Great. Can you make the risks section sharper?" }, { id: "t4", role: "assistant", state: "done", text: "Yes. I would name the two risks that can actually derail the quarter and drop the rest: scope creep on onboarding, and engineering capacity in November. Each gets an owner and an early warning sign, so you know in week three, not week eleven.", }, ] const sourceList = [ { title: "Planning a quarter with three bets", url: "https://example.com/three-bets", site: "Design Ops Handbook", snippet: "Teams that limit themselves to three bets ship more of them." }, { title: "Why research gets cut first", url: "https://example.org/research-squeezed", site: "Field Notes", snippet: "A standing weekly slot protects research better than a project plan does." }, ] type MuseChatProps = React.ComponentProps<"div"> & { /** "conversation" starts with a finished chat and an open document; "empty" starts a new chat with suggestions. */ start?: "conversation" | "empty" hrefs?: Partial<MuseHrefs> } /** The Muse chat: a calm reading column with reasoning, sources and a document panel, plus a model picker and a streamed reply to anything you send. */ function MuseChat({ start = "conversation", hrefs, ...props }: MuseChatProps) { const empty = start === "empty" const [turns, setTurns] = React.useState<Turn[]>(empty ? [] : starter) const [model, setModel] = React.useState("muse-5") const [panel, setPanel] = React.useState(!empty) const timers = React.useRef<ReturnType<typeof setTimeout>[]>([]) const count = React.useRef(0) React.useEffect(() => () => timers.current.forEach(clearTimeout), []) const busy = turns.some((t) => t.state === "thinking" || t.state === "streaming") function send(text: string) { if (busy || !text.trim()) return const n = turns.length const reply = replies[count.current++ % replies.length] setTurns((all) => [...all, { id: `u${n}`, role: "user", text }, { id: `a${n}`, role: "assistant", text: "", state: "thinking" }]) timers.current.push(setTimeout(() => setTurns((all) => all.map((t) => (t.id === `a${n}` ? { ...t, text: reply, state: "streaming" } : t))), 900)) } function finish(id: string) { setTurns((all) => all.map((t) => (t.id === id ? { ...t, state: "done" } : t))) } const greeting = ( <div className="mx-auto flex max-w-xl flex-col items-center px-4 pt-[10dvh] text-center"> <MuseMark className="size-12" /> <h2 className="mt-6 text-3xl font-medium tracking-[-0.02em] [font-family:var(--muse-serif),ui-serif,Georgia,serif] sm:text-4xl">Good morning, Mina.</h2> <p className="text-muted-foreground mt-3 text-pretty">What would you like to think through today?</p> </div> ) return ( <MuseShell page={empty ? "new" : "chat"} activeChat={empty ? undefined : "c1"} title={empty ? "New chat" : "Q4 plan for the design team"} hrefs={hrefs} fill actions={ <> <ModelPicker models={models} value={model} onValueChange={setModel} className="max-w-44" /> {!empty && <button type="button" className={cn(museButton.outline, "hidden sm:inline-flex")}><Share2 aria-hidden="true" /> Share</button>} </> } {...props} > <div className="relative flex min-h-0 min-w-0 flex-1"> <Chat className="min-w-0 flex-1"> <ChatMessages contentClassName="mx-auto w-full max-w-2xl px-4 py-6"> {turns.length === 0 && greeting} <div className="space-y-8"> {turns.map((t) => t.role === "user" ? ( <Message key={t.id} role="user"><MessageContent className="bg-secondary rounded-2xl px-4 py-3 text-[15px] text-pretty">{t.text}</MessageContent></Message> ) : ( <Message key={t.id} role="assistant"> <MessageAvatar className="bg-transparent"><MuseMark className="size-7" /></MessageAvatar> <MessageContent className="min-w-0 space-y-4"> {t.state === "thinking" && <ThinkingIndicator variant="dots" label="Thinking" />} {t.reasoning && <ReasoningDisclosure duration={9}><p className="text-sm text-pretty">{t.reasoning}</p></ReasoningDisclosure>} {t.text && ( <StreamingText text={t.text} animate={t.state === "streaming"} streaming={t.state === "streaming"} speed={170} onAnimationComplete={() => finish(t.id)} className="text-[17px] leading-8 text-pretty [font-family:var(--muse-serif),ui-serif,Georgia,serif]" /> )} {t.sources && ( <p className="text-muted-foreground text-sm"> Drawing on <Citation sources={sourceList} variant="pill" /> two sources about planning with limited bets. </p> )} {t.artifact && ( <button type="button" onClick={() => setPanel(true)} className="bg-card hover:bg-accent/50 focus-visible:ring-ring/50 flex w-full max-w-sm items-center gap-3 rounded-2xl border p-3 text-start outline-none transition-colors focus-visible:ring-[3px]"> <span aria-hidden="true" className="bg-chart-1/15 text-chart-1 flex size-10 items-center justify-center rounded-xl"><FileText className="size-5" /></span> <span className="min-w-0 flex-1"><span className="block text-sm font-medium">{plan.title}</span><span className="text-muted-foreground block text-xs">Document · open to edit</span></span> </button> )} </MessageContent> {t.state === "done" && t.text && <MessageActions><MessageCopyAction value={t.text} /></MessageActions>} </Message> ) )} </div> </ChatMessages> <ChatFooter contentClassName="mx-auto w-full max-w-2xl px-4 pb-5"> {turns.length === 0 && <SuggestionChips className="mb-4" variant="cards" label="Try one of these" suggestions={suggestions} onSelect={(p) => send(p)} />} <PromptInput onSubmit={(v) => send(v)} status={busy ? "streaming" : "idle"} className="rounded-2xl"> <PromptInputTextarea placeholder="Message Muse" aria-label="Message Muse" /> <PromptInputToolbar> <span className="text-muted-foreground flex items-center gap-1.5 text-xs"><Sparkles className="size-3.5" aria-hidden="true" />{models.find((m) => m.id === model)?.name}</span> <PromptInputSubmit className="ms-auto" /> </PromptInputToolbar> </PromptInput> <p className="text-muted-foreground mt-2 text-center text-xs">Muse can make mistakes. Check anything that matters.</p> </ChatFooter> </Chat> {panel && !empty && ( <div className="bg-background absolute inset-0 z-10 flex xl:static xl:z-auto xl:w-[26rem] xl:shrink-0 xl:border-s 2xl:w-[32rem]"> <ArtifactPanel title={plan.title} kind="Document" icon={<FileText />} code={plan.body} filename={plan.filename} language={plan.language} onClose={() => setPanel(false)} className="h-full w-full rounded-none border-0"> <div className="space-y-5 p-6 [font-family:var(--muse-serif),ui-serif,Georgia,serif]"> <h2 className="text-2xl font-semibold tracking-tight">Q4 design plan</h2> <section><h3 className="text-chart-1 text-sm font-semibold tracking-wide uppercase [font-family:var(--muse-sans)]">Goals</h3><ul className="mt-2 list-disc space-y-1.5 ps-5 text-[16px] leading-7"><li>Ship the new onboarding flow by Oct 31</li><li>Cut design-to-dev handoff time in half</li><li>Run two customer research rounds</li></ul></section> <section><h3 className="text-chart-1 text-sm font-semibold tracking-wide uppercase [font-family:var(--muse-sans)]">Three bets</h3><ol className="mt-2 list-decimal space-y-1.5 ps-5 text-[16px] leading-7"><li>Onboarding rebuild</li><li>Component library v2</li><li>Research ops</li></ol></section> <section><h3 className="text-chart-1 text-sm font-semibold tracking-wide uppercase [font-family:var(--muse-sans)]">Risks and owners</h3><ul className="mt-2 list-disc space-y-1.5 ps-5 text-[16px] leading-7"><li>Scope creep on onboarding (Priya)</li><li>Engineering capacity in November (Dev)</li></ul></section> </div> </ArtifactPanel> </div> )} </div> </MuseShell> ) } export { MuseChat, type MuseChatProps }components/ballmac/templates/muse/muse-projects.tsx// Ballmac UI: Muse projects page. https://ui.ballmac.com/templates/template-muse "use client" import * as React from "react" import { Check, FileText, MessageSquare, Plus, Upload } from "lucide-react" import { projects as seed, type MuseProject } from "@/components/ballmac/templates/muse/muse-data" import { MuseShell, museButton, type MuseHrefs } from "@/components/ballmac/templates/muse/muse-theme" import { cn } from "@/lib/utils" const tones = ["bg-chart-1/20", "bg-chart-2/20", "bg-chart-3/25", "bg-chart-4/20", "bg-chart-5/20"] type MuseProjectsProps = React.ComponentProps<"div"> & { hrefs?: Partial<MuseHrefs> } /** The Muse projects page: a list on the left, and the selected project with editable instructions, files and chats on the right. */ function MuseProjects({ hrefs, ...props }: MuseProjectsProps) { const [list, setList] = React.useState<MuseProject[]>(seed) const [selectedId, setSelectedId] = React.useState(seed[0].id) const [drafts, setDrafts] = React.useState<Record<string, string>>({}) const [savedId, setSavedId] = React.useState<string | null>(null) const sel = list.find((p) => p.id === selectedId) ?? list[0] const draft = drafts[sel.id] ?? sel.instructions const dirty = draft !== sel.instructions function save() { setList((all) => all.map((p) => (p.id === sel.id ? { ...p, instructions: draft } : p))) setSavedId(sel.id) } function addFile() { setList((all) => all.map((p) => (p.id === sel.id ? { ...p, files: [...p.files, { name: `Upload ${p.files.length + 1}.pdf`, size: "1.2 MB" }] } : p))) } function create() { const id = `new-${list.length}` setList((all) => [...all, { id, name: `Untitled project ${all.length - seed.length + 1}`, description: "A fresh project. Add instructions and files.", tone: (all.length % 4) + 1, chats: 0, files: [], instructions: "" }]) setSelectedId(id) } return ( <MuseShell page="projects" title="Projects" hrefs={hrefs} actions={<button type="button" className={museButton.primary} onClick={create}><Plus aria-hidden="true" /> New project</button>} {...props}> <main className="mx-auto grid max-w-5xl gap-6 px-4 pb-12 sm:px-6 lg:grid-cols-[18rem_minmax(0,1fr)]"> <section aria-label="All projects"> <ul className="space-y-2"> {list.map((p) => { const on = p.id === sel.id return ( <li key={p.id}> <button type="button" aria-pressed={on} onClick={() => setSelectedId(p.id)} className={cn("focus-visible:ring-ring/50 flex w-full items-center gap-3 rounded-2xl border p-3 text-start outline-none transition-colors focus-visible:ring-[3px]", on ? "bg-card border-foreground/25 shadow-sm" : "hover:bg-accent/60 border-transparent")}> <span className={cn("flex size-11 shrink-0 items-center justify-center rounded-xl text-base font-semibold", tones[(p.tone - 1) % tones.length])} aria-hidden="true">{p.name[0]}</span> <span className="min-w-0"><span className="block truncate text-sm font-medium">{p.name}</span><span className="text-muted-foreground block truncate text-xs">{p.chats} chats · {p.files.length} {p.files.length === 1 ? "file" : "files"}</span></span> </button> </li> ) })} </ul> </section> <section aria-labelledby="mp-title" className="min-w-0"> <div className="bg-card rounded-3xl border p-6 sm:p-8"> <div className="flex items-start gap-4"> <span className={cn("flex size-14 shrink-0 items-center justify-center rounded-2xl text-xl font-semibold", tones[(sel.tone - 1) % tones.length])} aria-hidden="true">{sel.name[0]}</span> <div className="min-w-0"><h2 id="mp-title" className="text-2xl font-medium tracking-[-0.02em] [font-family:var(--muse-serif),ui-serif,Georgia,serif]">{sel.name}</h2><p className="text-muted-foreground mt-1 text-pretty">{sel.description}</p></div> </div> <div className="mt-8"> <label htmlFor="mp-inst" className="text-sm font-medium">Instructions</label> <p className="text-muted-foreground mt-0.5 text-sm">Muse follows these in every chat in this project.</p> <textarea id="mp-inst" rows={4} value={draft} onChange={(e) => { setDrafts((d) => ({ ...d, [sel.id]: e.target.value })); setSavedId(null) }} placeholder="Tell Muse how to work on this project" className="bg-background focus-visible:ring-ring/50 placeholder:text-muted-foreground mt-3 w-full resize-y rounded-2xl border p-4 text-[15px] leading-relaxed outline-none focus-visible:ring-[3px]" /> <div className="mt-3 flex items-center justify-between gap-3"> <p className="text-muted-foreground flex items-center gap-1.5 text-sm" role="status">{savedId === sel.id ? <><Check className="text-chart-2 size-4" aria-hidden="true" />Saved</> : dirty ? "Unsaved changes" : ""}</p> <button type="button" onClick={save} disabled={!dirty} className={museButton.primary}>Save instructions</button> </div> </div> <div className="mt-8"> <div className="flex items-center justify-between"> <h3 className="text-sm font-medium">Files <span className="text-muted-foreground font-normal">({sel.files.length})</span></h3> <button type="button" onClick={addFile} className={museButton.outline}><Upload aria-hidden="true" /> Add file</button> </div> {sel.files.length === 0 ? ( <p className="text-muted-foreground mt-3 rounded-2xl border border-dashed p-6 text-center text-sm">No files yet. Add documents Muse should always have on hand.</p> ) : ( <ul className="mt-3 divide-y rounded-2xl border"> {sel.files.map((f) => <li key={f.name} className="flex items-center gap-3 px-4 py-3 text-sm"><FileText className="text-muted-foreground size-4" aria-hidden="true" /><span className="min-w-0 flex-1 truncate font-medium">{f.name}</span><span className="text-muted-foreground text-xs">{f.size}</span></li>)} </ul> )} </div> <div className="mt-8"> <h3 className="text-sm font-medium">Chats in this project</h3> <ul className="mt-3 grid gap-2 sm:grid-cols-2"> {["Weekly update draft", "Planning notes", "Feedback summary"].slice(0, Math.min(3, Math.max(1, sel.chats))).map((c) => ( <li key={c}><a href={hrefs?.chat ?? "/muse"} className="hover:bg-accent focus-visible:ring-ring/50 flex items-center gap-2.5 rounded-xl border p-3 text-sm outline-none focus-visible:ring-[3px]"><MessageSquare className="text-muted-foreground size-4" aria-hidden="true" />{c}</a></li> ))} </ul> {sel.chats === 0 && <p className="text-muted-foreground mt-2 text-sm">No chats yet. Start one from the sidebar.</p>} </div> </div> </section> </main> </MuseShell> ) } export { MuseProjects, type MuseProjectsProps }components/ballmac/templates/muse/muse-library.tsx// Ballmac UI: Muse library page. https://ui.ballmac.com/templates/template-muse "use client" import * as React from "react" import { Database, FileCode2, FileText, Search } from "lucide-react" import { CopyButton } from "@/components/ballmac/copy-button" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ballmac/dialog" import { artifacts, type MuseArtifact } from "@/components/ballmac/templates/muse/muse-data" import { MuseShell, type MuseHrefs } from "@/components/ballmac/templates/muse/muse-theme" import { cn } from "@/lib/utils" type Filter = "All" | MuseArtifact["kind"] const filters: Filter[] = ["All", "Document", "Code", "Data"] const icons = { Document: FileText, Code: FileCode2, Data: Database } type MuseLibraryProps = React.ComponentProps<"div"> & { hrefs?: Partial<MuseHrefs> } /** The Muse library: everything Muse has written for you, filterable by type, with a preview you can copy from. */ function MuseLibrary({ hrefs, ...props }: MuseLibraryProps) { const [filter, setFilter] = React.useState<Filter>("All") const [query, setQuery] = React.useState("") const [openId, setOpenId] = React.useState<string | null>(null) const q = query.trim().toLowerCase() const shown = artifacts.filter((a) => (filter === "All" || a.kind === filter) && (!q || a.title.toLowerCase().includes(q))) const open = artifacts.find((a) => a.id === openId) return ( <MuseShell page="library" title="Library" hrefs={hrefs} {...props}> <main className="mx-auto max-w-5xl px-4 pb-12 sm:px-6"> <p className="text-muted-foreground max-w-xl text-pretty">Documents, code and data Muse has made for you, kept together so you can find them again.</p> <div className="mt-6 flex flex-wrap items-center gap-3"> <div role="group" aria-label="Filter by type" className="flex gap-1.5"> {filters.map((f) => ( <button key={f} type="button" aria-pressed={filter === f} onClick={() => setFilter(f)} className={cn("focus-visible:ring-ring/50 h-9 rounded-full border px-4 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px]", filter === f ? "bg-primary text-primary-foreground border-transparent" : "bg-card hover:bg-accent")}> {f} </button> ))} </div> <div className="relative ms-auto w-full sm:w-64"> <Search className="text-muted-foreground pointer-events-none absolute top-1/2 start-3.5 size-4 -translate-y-1/2" aria-hidden="true" /> <input type="search" aria-label="Search the library" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search" className="bg-card focus-visible:ring-ring/50 placeholder:text-muted-foreground h-9 w-full rounded-full border pe-4 ps-10 text-sm outline-none focus-visible:ring-[3px]" /> </div> </div> <p className="sr-only" role="status">{shown.length} items</p> {shown.length === 0 ? ( <div className="bg-card text-muted-foreground mt-8 rounded-3xl border border-dashed px-6 py-20 text-center text-sm">Nothing here yet. Ask Muse to write something and it will land in your library.</div> ) : ( <ul className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> {shown.map((a) => { const Icon = icons[a.kind] return ( <li key={a.id}> <button type="button" onClick={() => setOpenId(a.id)} className="bg-card hover:border-foreground/25 focus-visible:ring-ring/50 group flex h-full w-full flex-col overflow-hidden rounded-3xl border text-start outline-none transition-all hover:shadow-md focus-visible:ring-[3px]"> <div aria-hidden="true" className={cn("bg-surface h-36 overflow-hidden border-b p-5 text-[11px] leading-5", a.kind === "Code" || a.kind === "Data" ? "font-mono" : "[font-family:var(--muse-serif),ui-serif,Georgia,serif] text-[13px]")}> {a.lines.map((l, i) => <p key={i} className={cn("truncate", i === 0 && a.kind === "Document" ? "text-foreground font-semibold" : "text-muted-foreground")}>{l}</p>)} </div> <div className="flex flex-1 items-center gap-3 p-4"> <span className="bg-chart-1/12 text-chart-1 flex size-9 shrink-0 items-center justify-center rounded-xl"><Icon className="size-4" aria-hidden="true" /></span> <span className="min-w-0"><span className="block truncate text-sm font-medium">{a.title}</span><span className="text-muted-foreground block text-xs">{a.kind} · {a.updated}</span></span> </div> </button> </li> ) })} </ul> )} </main> <Dialog open={!!open} onOpenChange={(o) => !o && setOpenId(null)}> <DialogContent className="max-w-2xl"> {open && ( <> <DialogHeader> <DialogTitle>{open.title}</DialogTitle> <DialogDescription>{open.kind} · {open.filename} · updated {open.updated.toLowerCase()}</DialogDescription> </DialogHeader> <div className="relative"> <pre tabIndex={0} aria-label={`${open.title} contents`} className={cn("bg-surface focus-visible:ring-ring/50 max-h-[50dvh] overflow-auto rounded-2xl border p-5 text-sm leading-7 whitespace-pre-wrap outline-none focus-visible:ring-[3px]", open.kind === "Document" ? "[font-family:var(--muse-serif),ui-serif,Georgia,serif] text-[15px]" : "font-mono text-[13px] leading-6")}>{open.body}</pre> <CopyButton value={open.body} ariaLabel="Copy contents" className="absolute top-2 end-2" /> </div> </> )} </DialogContent> </Dialog> </MuseShell> ) } export { MuseLibrary, type MuseLibraryProps }components/ballmac/templates/muse/muse-settings.tsx// Ballmac UI: Muse settings page. https://ui.ballmac.com/templates/template-muse "use client" import * as React from "react" import { Trash2 } from "lucide-react" import { Slider } from "@/components/ballmac/slider" import { SegmentedControl, SegmentedControlItem } from "@/components/ballmac/segmented-control" import { Switch } from "@/components/ballmac/switch" import { memories as seedMemories, models } from "@/components/ballmac/templates/muse/muse-data" import { MuseShell, museButton, type MuseHrefs } from "@/components/ballmac/templates/muse/muse-theme" import { cn } from "@/lib/utils" type Font = "serif" | "sans" type MuseSettingsProps = React.ComponentProps<"div"> & { hrefs?: Partial<MuseHrefs> } function Card({ id, title, description, children }: { id: string; title: string; description: string; children: React.ReactNode }) { return ( <section aria-labelledby={id} className="bg-card rounded-3xl border p-6 sm:p-7"> <h2 id={id} className="text-lg font-medium">{title}</h2> <p className="text-muted-foreground mt-1 text-sm text-pretty">{description}</p> <div className="mt-5">{children}</div> </section> ) } /** The Muse settings page: reading preferences with a live preview, default model, memory you can edit, and data controls with a type-to-confirm delete. */ function MuseSettings({ hrefs, ...props }: MuseSettingsProps) { const [font, setFont] = React.useState<Font>("serif") const [size, setSize] = React.useState(17) const [model, setModel] = React.useState("muse-5") const [memoryOn, setMemoryOn] = React.useState(true) const [memories, setMemories] = React.useState(seedMemories) const [confirm, setConfirm] = React.useState("") const [deleted, setDeleted] = React.useState(false) return ( <MuseShell page="settings" title="Settings" hrefs={hrefs} {...props}> <main className="mx-auto max-w-2xl space-y-5 px-4 pb-14 sm:px-6"> <Card id="ms-reading" title="Reading" description="Choose how replies look. The preview updates as you change it."> <div className="flex flex-wrap items-center justify-between gap-4"> <p id="ms-font-label" className="text-sm font-medium">Reply font</p> <SegmentedControl aria-labelledby="ms-font-label" value={font} onValueChange={(v) => setFont(v as Font)}> <SegmentedControlItem value="serif">Serif</SegmentedControlItem> <SegmentedControlItem value="sans">Sans</SegmentedControlItem> </SegmentedControl> </div> <div className="mt-6"> <div className="flex items-baseline justify-between text-sm"><label id="ms-size-label" className="font-medium">Text size</label><output className="text-muted-foreground tabular-nums">{size}px</output></div> <Slider className="mt-3" aria-labelledby="ms-size-label" min={15} max={21} step={1} value={[size]} onValueChange={(v) => setSize(v[0] ?? size)} formatValue={(v) => `${v} pixels`} /> </div> <p className={cn("bg-surface mt-6 rounded-2xl border p-5 text-pretty", font === "serif" ? "[font-family:var(--muse-serif),ui-serif,Georgia,serif]" : "")} style={{ fontSize: size, lineHeight: 1.75 }}> An index fund is a basket that owns a small slice of hundreds of companies, so you are not betting on any one of them. </p> </Card> <Card id="ms-model" title="Default model" description="New chats start with this model. You can change it in any chat."> <div role="radiogroup" aria-label="Default model" className="grid gap-2"> {models.map((m) => ( <label key={m.id} className={cn("has-[:focus-visible]:ring-ring/50 flex cursor-pointer items-start gap-3 rounded-2xl border p-4 transition-colors has-[:focus-visible]:ring-[3px]", model === m.id ? "border-foreground/40 bg-accent/60" : "hover:bg-accent/40")}> <input type="radio" name="default-model" value={m.id} checked={model === m.id} onChange={() => setModel(m.id)} className="accent-chart-1 mt-1 size-4" /> <span><span className="block text-sm font-medium">{m.name}</span><span className="text-muted-foreground block text-sm text-pretty">{m.description}</span></span> </label> ))} </div> </Card> <Card id="ms-memory" title="Memory" description="Muse remembers useful things between chats. You decide what stays."> <div className="flex items-center justify-between gap-4"> <p id="ms-mem-label" className="text-sm font-medium">Remember things about me</p> <Switch aria-labelledby="ms-mem-label" checked={memoryOn} onCheckedChange={setMemoryOn} /> </div> {memoryOn && ( <> <ul className="mt-4 divide-y rounded-2xl border"> {memories.map((m) => ( <li key={m} className="flex items-center gap-3 px-4 py-3 text-sm"> <span className="min-w-0 flex-1 text-pretty">{m}</span> <button type="button" aria-label={`Forget: ${m}`} onClick={() => setMemories((all) => all.filter((x) => x !== m))} className="text-muted-foreground hover:text-destructive hover:bg-accent focus-visible:ring-ring/50 inline-flex size-8 shrink-0 items-center justify-center rounded-lg outline-none focus-visible:ring-[3px]"><Trash2 className="size-4" aria-hidden="true" /></button> </li> ))} {memories.length === 0 && <li className="text-muted-foreground px-4 py-8 text-center text-sm">Muse hasn’t remembered anything yet.</li>} </ul> <p className="text-muted-foreground mt-2 text-xs" role="status">{memories.length} {memories.length === 1 ? "memory" : "memories"} saved</p> </> )} </Card> <Card id="ms-data" title="Your data" description="Export everything, or delete your account and all chats."> <button type="button" className={museButton.outline}>Export my data</button> <div className="border-destructive/40 mt-6 rounded-2xl border p-5"> <h3 className="text-sm font-medium">Delete account</h3> <p className="text-muted-foreground mt-1 text-sm text-pretty">This removes all chats, projects and your library. It can’t be undone.</p> {deleted ? ( <p role="status" className="text-destructive mt-4 text-sm font-medium">Your account is scheduled for deletion in 7 days.</p> ) : ( <> <label htmlFor="ms-confirm" className="mt-4 block text-sm">Type <span className="font-mono font-semibold">delete my account</span> to confirm</label> <input id="ms-confirm" value={confirm} onChange={(e) => setConfirm(e.target.value)} className="bg-background focus-visible:ring-ring/50 mt-2 h-10 w-full rounded-xl border px-3 text-sm outline-none focus-visible:ring-[3px]" /> <button type="button" disabled={confirm !== "delete my account"} onClick={() => setDeleted(true)} className={cn(museButton.danger, "mt-3")}>Delete account</button> </> )} </div> </Card> </main> </MuseShell> ) } export { MuseSettings, type MuseSettingsProps }app/muse/page.tsx// Ballmac UI: Muse template route. https://ui.ballmac.com/templates/template-muse import type { Metadata } from "next" import { MuseChat } from "@/components/ballmac/templates/muse/muse-chat" export const metadata: Metadata = { title: "Muse", description: "A calm place to think with an AI.", } export default function Page() { return <MuseChat start="conversation" /> }app/muse/new/page.tsx// Ballmac UI: Muse template route. https://ui.ballmac.com/templates/template-muse import type { Metadata } from "next" import { MuseChat } from "@/components/ballmac/templates/muse/muse-chat" export const metadata: Metadata = { title: "New chat · Muse", description: "Start a new conversation.", } export default function Page() { return <MuseChat start="empty" /> }app/muse/projects/page.tsx// Ballmac UI: Muse template route. https://ui.ballmac.com/templates/template-muse import type { Metadata } from "next" import { MuseProjects } from "@/components/ballmac/templates/muse/muse-projects" export const metadata: Metadata = { title: "Projects · Muse", description: "Instructions, files and chats for each project.", } export default function Page() { return <MuseProjects /> }app/muse/library/page.tsx// Ballmac UI: Muse template route. https://ui.ballmac.com/templates/template-muse import type { Metadata } from "next" import { MuseLibrary } from "@/components/ballmac/templates/muse/muse-library" export const metadata: Metadata = { title: "Library · Muse", description: "Everything Muse has made for you.", } export default function Page() { return <MuseLibrary /> }app/muse/settings/page.tsx// Ballmac UI: Muse template route. https://ui.ballmac.com/templates/template-muse import type { Metadata } from "next" import { MuseSettings } from "@/components/ballmac/templates/muse/muse-settings" export const metadata: Metadata = { title: "Settings · Muse", description: "Reading, models, memory and data.", } export default function Page() { return <MuseSettings /> }Update the import paths to match your project setup.
The page is added as a route in your app, with every block it uses.
What's inside
- Pages
- Chat/muse
- New chat/muse/new
- Projects/muse/projects
- Library/muse/library
- Settings/muse/settings
- Fonts
- DM Sans, Source Serif 4 (Google Fonts, loaded with next/font)
- Files
- 12 files · 16 Ballmac blocks and components
Built with
Dependencies
- npm
- lucide-react
- Registry
- @ballmac/ai-chat@ballmac/ai-message@ballmac/artifact-panel@ballmac/citation@ballmac/copy-button@ballmac/dialog@ballmac/model-picker@ballmac/prompt-input@ballmac/reasoning-disclosure@ballmac/segmented-control@ballmac/sheet@ballmac/slider@ballmac/streaming-text@ballmac/suggestion-chips@ballmac/switch@ballmac/thinking-indicatorshadcn/utils
Use with AI
Installs a five-page AI chat product with a serif reading column. Replace send() in muse-chat.tsx with your model call, muse-data.ts with your history, and edit museCss for the palette. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Muse: AI chat app (@ballmac/template-muse) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Chat assistants and copilots that need a full product UI
- Reading-heavy AI tools where answers are long
Not for
- Embedding a small chat widget in another page (use ai-chat-1)
Registry JSON: https://ui.ballmac.com/r/template-muse.json
Free to use in personal and commercial projects.