// --- View functions for dashboard states --- // All views follow three consistent sections: Floor, Devices, Projects & Sessions. import { nav, slackIcon, type NavOpts } from "./layout"; import type { SessionFixture, ProjectFixture, DeviceFixture } from "./fixtures"; import type { Annotation } from "../types"; interface TeamMember { name: string; email: string; } interface ViewContext { token: string; userName: string; orgName: string; orgSlug: string | null; email: string; slackConnected: boolean; floorSkipped: boolean; cliInstalled: boolean; hasConnectedSession: boolean; totalPrompts: number; teamMembers?: TeamMember[]; plan?: string; dailyPrompts?: Array<{ date: string; sender: string; count: number }>; } function navOpts(ctx: { userName: string; orgName: string; email: string; plan?: string }): NavOpts { return { userName: ctx.userName, orgName: ctx.orgName, email: ctx.email, plan: ctx.plan, banner: bannerForCtx(ctx) }; } // Minimal context for pages that don't need the full dashboard state (transcript, search). export interface PageContext { token: string; userName: string; orgName: string; email: string; } function escapeHtml(text: string): string { return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } // --- Copyable code block --- function copyBlock(text: string): string { const id = `copy-${Math.random().toString(36).slice(2, 8)}`; return `
${text}
`; } // --- Shared section renderers --- function sectionHeader(title: string): string { return `

${title}

`; } const formInputClass = "border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-polaris-300 focus:border-polaris-300"; function statusBadge(label: string, done: boolean): string { return done ? ` ${label} ` : `${label}`; } // --- Banner helpers --- function bannerForCtx(ctx: { plan?: string }): { message: string; style: "info" | "success" | "warning" } | undefined { if (ctx.plan && ctx.plan !== "free") { const label = ctx.plan.charAt(0).toUpperCase() + ctx.plan.slice(1); return { message: `${label} plan — we'll reach out shortly to get you set up. Full access in the meantime.`, style: "info" }; } return undefined; } // --- Team members --- function renderTeamMembers(members: TeamMember[], currentEmail: string): string { if (members.length === 0) return ""; return `
${members.map((m) => { const isYou = m.email === currentEmail; return `
${m.name.charAt(0).toUpperCase()}

${m.name}${isYou ? ' (you)' : ""}

${m.email}
`; }).join("")}
`; } // --- Daily prompts chart --- function renderDailyPrompts(data: Array<{ date: string; sender: string; count: number }> | undefined): string { if (!data || data.length === 0) return ""; const id = `heatmap-${Math.random().toString(36).slice(2, 8)}`; // Build date columns for last 14 days const dates: string[] = []; const now = new Date(); for (let i = 13; i >= 0; i--) { const d = new Date(now); d.setDate(d.getDate() - i); dates.push(d.toISOString().slice(0, 10)); } // Totals per day const dayTotals = new Map(); let total = 0; for (const d of data) { dayTotals.set(d.date, (dayTotals.get(d.date) ?? 0) + d.count); total += d.count; } // Group by sender const bySender = new Map>(); for (const d of data) { if (!bySender.has(d.sender)) bySender.set(d.sender, new Map()); bySender.get(d.sender)!.set(d.date, d.count); } function intensity(count: number, max: number): string { if (count === 0) return "bg-gray-100"; const ratio = count / max; if (ratio < 0.25) return "bg-polaris-100"; if (ratio < 0.5) return "bg-polaris-200"; if (ratio < 0.75) return "bg-polaris-400"; return "bg-polaris-600"; } function shortName(sender: string): string { const name = sender.replace(/^user:/, "").split(".")[0]; return name.charAt(0).toUpperCase() + name.slice(1); } // Team view — histogram bars const teamMax = Math.max(...dates.map((d) => dayTotals.get(d) ?? 0), 1); const teamBars = dates.map((date) => { const count = dayTotals.get(date) ?? 0; const pct = Math.max((count / teamMax) * 100, count > 0 ? 12 : 0); const label = date.slice(5).replace("-", "/"); const day = new Date(date + "T12:00:00").toLocaleDateString("en-US", { weekday: "short" }); return `
${count > 0 ? `${count}` : ` `}
${label}
`; }).join(""); const teamView = `
${teamBars}
`; // Per-user view — each row uses its own max for color scale const userRows = Array.from(bySender.entries()).map(([sender, counts]) => { const rowMax = Math.max(...dates.map((d) => counts.get(d) ?? 0), 1); const cells = dates.map((date) => { const count = counts.get(date) ?? 0; const day = new Date(date + "T12:00:00").toLocaleDateString("en-US", { weekday: "short" }); return `
`; }).join(""); return `
${shortName(sender)}
${cells}
`; }).join(""); const dateLabels = dates.map((date) => { return `
${date.slice(5).replace("-", "/")}
`; }).join(""); const userView = `
${userRows}
${dateLabels}
`; return `

Prompts captured

${total} last 14d
${teamView}
`; } // --- Floor section --- type StepState = "done" | "active" | "future"; function cardClass(state: StepState): string { if (state === "active") return "border-polaris-300 bg-polaris-50/30 shadow-sm"; return "border-gray-200"; } function sectionWrap(state: StepState, content: string): string { if (state === "future") return `
${content}
`; return content; } function renderFloorSection(ctx: ViewContext, compact = false, state: StepState = "done"): string { if (compact && ctx.slackConnected) { const promptStat = ctx.totalPrompts > 0 ? `${ctx.totalPrompts} prompt${ctx.totalPrompts !== 1 ? "s" : ""}` : ''; const teamCount = ctx.teamMembers?.length ?? 0; return `

Floor

${statusBadge("Connected", true)} ${teamCount > 0 ? `${teamCount} member${teamCount !== 1 ? "s" : ""}` : ""}
${slackIcon.replace('class="w-4 h-4"', 'class="w-4 h-4 text-white"')}

Slack

${ctx.orgSlug ? `${ctx.orgSlug}` : ''} ${promptStat}
${renderDailyPrompts(ctx.dailyPrompts)} ${ctx.teamMembers ? renderTeamMembers(ctx.teamMembers, ctx.email) : ""}
`; } if (ctx.slackConnected) { const slugLabel = ctx.orgSlug ? `${ctx.orgSlug}` : ''; const teamCount = ctx.teamMembers?.length ?? 0; return `

Floor

${statusBadge("Connected", true)} ${teamCount > 0 ? `${teamCount} member${teamCount !== 1 ? "s" : ""}` : ""}
${slackIcon.replace('class="w-4 h-4"', 'class="w-4 h-4 text-white"')}

Slack

${slugLabel}
${renderDailyPrompts(ctx.dailyPrompts)} ${ctx.teamMembers ? renderTeamMembers(ctx.teamMembers, ctx.email) : ""}
`; } if (ctx.floorSkipped) { return `

Floor

${statusBadge("Skipped", false)}

No floor connected.

Connect Slack
`; } return sectionWrap(state, `

Floor — optional

Connect a floor

Share sessions with your team in real time. Connect Slack to publish your session log to a channel.

`); } // --- Devices section --- function renderDevicesSection(ctx: ViewContext, devices: DeviceFixture[], state: StepState = "done"): string { if (devices.length > 0) { return `

Devices

${statusBadge(`${devices.length} connected`, true)}
+ Add another device

Run on any new machine:

${copyBlock("npx @lightupai/polaris")}
${devices.map((d) => renderDeviceRow(d)).join("")}
`; } // Setup state — no devices yet return sectionWrap(state, `

Devices

${ctx.cliInstalled ? statusBadge("Installed", true) : statusBadge("Not installed", false)}

${ctx.cliInstalled ? "Add another device" : "Set up Polaris on your first device"}

Run this in your terminal${ctx.cliInstalled ? " on any new machine" : ". Repeat on each machine you work from"}.

${copyBlock("npx @lightupai/polaris")}
`); } function renderDeviceRow(device: DeviceFixture): string { const recentThreshold = Date.now() - 60 * 60 * 1000; // 1 hour const isOnline = device.activeSession || new Date(device.lastSeen).getTime() > recentThreshold; return `

${device.name}

${isOnline ? '' : ''}

${device.os}

${device.activeSession ? `

${device.activeSession}

` : ""}

${isOnline ? "Online" : "Offline"} · ${new Date(device.lastSeen).toLocaleString()}

`; } // --- Projects & Sessions section --- function renderProjectsSessionsSection(ctx: ViewContext, sessions: SessionFixture[], projects: ProjectFixture[], state: StepState = "done"): string { if (projects.length > 0) { const totalSessions = projects.reduce((n, p) => n + p.sessions.length, 0); return `

Projects & Sessions

${statusBadge(`${totalSessions} active`, true)}
+ Join another session

Inside your AI agent, run:

${copyBlock("/polaris join #channel-name")}
${projects.map((p) => renderProjectCard(p, ctx.userName, ctx.token)).join("")}
`; } // Setup state — no sessions yet return sectionWrap(state, `

Projects & Sessions

${ctx.hasConnectedSession ? statusBadge("Connected", true) : statusBadge("Waiting", false)}

Connect your first session

${ctx.hasConnectedSession ? "You've connected a session. You're ready to collaborate." : "Inside your AI agent (Claude Code, Cursor, etc.), run:"}

${ctx.hasConnectedSession ? "" : copyBlock("/polaris join #my-project")} ${ctx.hasConnectedSession ? "" : `

The name becomes your project workspace — and your Slack channel if a floor is connected.

`}
`); } function renderProjectCard(project: ProjectFixture, userName: string, token: string): string { const sessionCount = project.sessions.length; const participantId = `user:${userName.toLowerCase().replace(/\s+/g, ".")}`; return `

${project.name}

${project.slackChannel ? `${project.slackChannel}` : ""}
${sessionCount} session${sessionCount !== 1 ? "s" : ""} Settings
${project.sessions.map((s) => { const isDriver = s.driver === participantId; const roleBadge = isDriver ? 'Driver' : s.driver ? 'Advisor' : ''; const driverLabel = s.driver && !isDriver ? `${s.driver}` : ''; const promptLabel = s.eventCount > 0 ? `${s.eventCount} prompt${s.eventCount !== 1 ? "s" : ""}` : ''; const transcriptHref = `/sessions/${encodeURIComponent(project.name)}/${encodeURIComponent(s.name)}?token=${token}`; return `
${s.name} ${roleBadge}
${driverLabel} ${promptLabel} Transcript
`; }).join("")}
`; } // --- Auto-refresh script --- function autoRefreshScript(token: string): string { return ` `; } // --- Setup view (zero state) --- // Same three sections, but each shows its setup prompt instead of live data. export function renderSetupView(ctx: ViewContext, devices: DeviceFixture[] = []): string { const floorDone = ctx.slackConnected || ctx.floorSkipped; const nextStep = !floorDone ? "floor" : !ctx.cliInstalled ? "devices" : "sessions"; const stepState = (step: string): "done" | "active" | "future" => { const order = ["floor", "devices", "sessions"]; const nextIdx = order.indexOf(nextStep); const thisIdx = order.indexOf(step); if (thisIdx < nextIdx) return "done"; if (thisIdx === nextIdx) return "active"; return "future"; }; return ` ${nav(ctx.token, navOpts(ctx))}
${renderFloorSection(ctx, false, stepState("floor"))} ${renderDevicesSection(ctx, ctx.cliInstalled ? devices : [], stepState("devices"))} ${renderProjectsSessionsSection(ctx, [], [], stepState("sessions"))}
${autoRefreshScript(ctx.token)}`; } // --- Error view --- export function renderErrorView(message: string, linkText?: string, linkHref?: string): string { return ` ${nav()}

${message}

${linkText && linkHref ? `${linkText}` : ""}
`; } // --- Active view (user has sessions) --- // Same three sections, populated with live data. export function renderActiveView(ctx: ViewContext, sessions: SessionFixture[], projects: ProjectFixture[], devices: DeviceFixture[] = []): string { return ` ${nav(ctx.token, navOpts(ctx))}
${renderFloorSection(ctx, true)} ${renderDevicesSection(ctx, devices)} ${renderProjectsSessionsSection(ctx, sessions, projects)}
${autoRefreshScript(ctx.token)}`; } // --- Profile view --- export function renderProfileView(ctx: ViewContext, participantId: string): string { return ` ${nav(ctx.token, navOpts(ctx))}
${sectionHeader("Profile")}
${ctx.userName.charAt(0).toUpperCase()}

${ctx.userName}

${ctx.email}

Participant ID

${participantId}

Organization

${ctx.orgName}

${sectionHeader("API token")}

Use this for scripts and direct API access. The polaris login CLI handles this automatically.

${copyBlock(ctx.token)}
`; } // --- Session transcript view --- // Events arrive from the API newest-first ({ events, nextCursor }); we render ascending. export interface TranscriptEvent { id: string; project: string; session: string; timestamp: string; source: string; sender: string; payload: Record; } interface EventPayload { hook_event_name?: string; prompt?: string; stop_response?: string; last_assistant_message?: string; tool_name?: string; tool_input?: unknown; tool_result?: unknown; content?: string; } function eventMeta(e: TranscriptEvent): string { return `${escapeHtml(e.sender)} · ${new Date(e.timestamp).toLocaleString()}`; } function messageCard(badge: string, badgeClass: string, e: TranscriptEvent, text: string, cardClass = "bg-white border border-gray-200"): string { return `
${badge} ${eventMeta(e)}

${escapeHtml(text)}

`; } function renderTranscriptItem(e: TranscriptEvent): string { const p = e.payload as EventPayload; if (e.source === "inject") { return `
Inject ${eventMeta(e)}

[${escapeHtml(e.sender)}] ${escapeHtml(p.content ?? "")}

`; } if (e.source === "reply") { return messageCard("Reply", "bg-gray-100 text-gray-600", e, p.content ?? ""); } if (typeof p.prompt === "string") { return messageCard("Prompt", "bg-polaris-100 text-polaris-800", e, p.prompt); } if (typeof p.tool_name === "string") { const detail = p.hook_event_name === "PostToolUse" ? p.tool_result : p.tool_input; let detailText = ""; try { detailText = JSON.stringify(detail, null, 2) ?? ""; } catch { detailText = String(detail); } return `
${escapeHtml(p.tool_name)} ${p.hook_event_name === "PostToolUse" ? "result" : "tool call"} ${eventMeta(e)} ${detailText ? `
${escapeHtml(detailText)}
` : ""}
`; } const response = typeof p.stop_response === "string" && p.stop_response ? p.stop_response : p.last_assistant_message; if (typeof response === "string" && response) { return messageCard("Agent", "bg-green-100 text-green-800", e, response, "bg-white border border-gray-200"); } return ""; } // --- Annotation controls (star / tag / decision curation) --- // Short excerpt of the event's main text, used as the default note when marking a decision. function eventExcerpt(e: TranscriptEvent): string { const p = e.payload as EventPayload; const text = typeof p.prompt === "string" ? p.prompt : typeof p.content === "string" ? p.content : typeof p.stop_response === "string" && p.stop_response ? p.stop_response : typeof p.last_assistant_message === "string" ? p.last_assistant_message : ""; return text.slice(0, 200); } function annotationForm(action: string, fields: Record, button: string): string { const inputs = Object.entries(fields) .map(([name, value]) => ``) .join(""); return `
${inputs}${button}
`; } function renderAnnotationControls(e: TranscriptEvent, annotations: Annotation[], base: string, token: string): string { const own = annotations.filter((a) => a.event_id === e.id); const star = own.find((a) => a.kind === "star"); const tags = own.filter((a) => a.kind === "tag"); const decision = own.find((a) => a.kind === "decision"); const addAction = `${base}/annotations?token=${token}`; const deleteAction = (id: string) => `${base}/annotations/${encodeURIComponent(id)}/delete?token=${token}`; const starControl = star ? annotationForm(deleteAction(star.id), {}, ``) : annotationForm(addAction, { event_id: e.id, kind: "star" }, ``); const tagChips = tags .map((t) => annotationForm(deleteAction(t.id), {}, ``) ) .join(""); const tagAdder = `
+ Tag
`; const decisionControl = decision ? annotationForm(deleteAction(decision.id), {}, ``) : annotationForm(addAction, { event_id: e.id, kind: "decision", value: eventExcerpt(e) }, ``); return `
${starControl} ${tagChips} ${tagAdder} ${decisionControl}
`; } export function renderTranscriptView( ctx: PageContext, project: string, session: string, events: TranscriptEvent[], nextCursor: string | null, before?: string, annotations: Annotation[] = [] ): string { const base = `/sessions/${encodeURIComponent(project)}/${encodeURIComponent(session)}`; const items = [...events] .reverse() .map((e) => { const html = renderTranscriptItem(e); return html ? `
${html}${renderAnnotationControls(e, annotations, base, ctx.token)}
` : ""; }) .filter((html) => html !== "") .join("\n"); // Session-level annotations (no event_id) render as badges under the header. const sessionBadges = annotations .filter((a) => a.event_id === null) .map((a) => a.kind === "tag" ? `#${escapeHtml(a.value ?? "")}` : a.kind === "decision" ? `⚑ ${escapeHtml(a.value ?? "Decision")}` : `★` ) .join(""); const pagingLinks = [ before ? `Back to latest` : "", nextCursor ? `Load older` : "", ].filter(Boolean).join(""); return ` ${nav(ctx.token, navOpts(ctx))}
${sectionHeader("Session transcript")}

${escapeHtml(session)}

${escapeHtml(project)}
${sessionBadges ? `
${sessionBadges}
` : ""} ${pagingLinks ? `
${pagingLinks}
` : ""} ${items ? `
${items}
` : `
No events captured for this session yet.
`}
${sectionHeader("Inject guidance")}

Send a note to this session's agent. It is delivered with the next prompt.

`; } // --- Search view --- export interface SearchQuery { q: string; project: string; sender: string; source: string; tag: string; } export interface SearchResult { event: TranscriptEvent; snippet: string; } // ts_headline marks matches with ...; escape everything else, then restore the highlights. function snippetHtml(snippet: string): string { return escapeHtml(snippet) .replace(/<b>/g, '') .replace(/<\/b>/g, ""); } export function renderSearchView(ctx: PageContext, query: SearchQuery, results: SearchResult[] | null, searchError?: string): string { const inputClass = "border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-polaris-300 focus:border-polaris-300"; const sourceOptions = ["", "hook", "inject", "reply"] .map((s) => ``) .join(""); let resultsSection = ""; if (searchError) { resultsSection = `
${escapeHtml(searchError)}
`; } else if (results !== null) { const resultItems = results.map(({ event, snippet }) => { const href = `/sessions/${encodeURIComponent(event.project)}/${encodeURIComponent(event.session)}?token=${ctx.token}`; return `
${escapeHtml(event.sender)} ${escapeHtml(event.project)}/${escapeHtml(event.session)} ${new Date(event.timestamp).toLocaleString()}

${snippetHtml(snippet)}

`; }).join(""); resultsSection = results.length ? `

${results.length} result${results.length !== 1 ? "s" : ""} for “${escapeHtml(query.q)}”

${resultItems}
` : `
No results for “${escapeHtml(query.q)}”.
`; } return ` ${nav(ctx.token, navOpts(ctx))}
${sectionHeader("Search")}
${resultsSection}
`; } // --- Decisions view --- // Org-wide list of 'decision' annotations, each linking back to its transcript. export function renderDecisionsView(ctx: PageContext, projectFilter: string, decisions: Annotation[] | null, error?: string): string { let listSection = ""; if (error) { listSection = `
${escapeHtml(error)}
`; } else if (decisions !== null) { const items = decisions.map((d) => { const href = `/sessions/${encodeURIComponent(d.project)}/${encodeURIComponent(d.session)}?token=${ctx.token}`; return `
⚑ Decision ${d.participant_id ? `${escapeHtml(d.participant_id)}` : ""} ${escapeHtml(d.project)}/${escapeHtml(d.session)} ${new Date(d.created_at).toLocaleString()}

${d.value ? escapeHtml(d.value) : 'No note'}

View transcript
`; }).join(""); listSection = decisions.length ? `
${items}
` : `
No decisions recorded yet. Mark transcript events as decisions to collect them here.
`; } return ` ${nav(ctx.token, navOpts(ctx))}
${sectionHeader("Decisions")}
${listSection}
`; } // --- Project settings view --- // Visibility toggle ('org' = everyone, 'members' = restricted) and member management. export interface ProjectMemberView { participant_id: string; role: string | null; } export function renderProjectSettingsView(ctx: PageContext, project: string, visibility: string, members: ProjectMemberView[], membersError?: string): string { const base = `/projects/${encodeURIComponent(project)}`; const isMembers = visibility === "members"; const memberRows = members.map((m) => `

${escapeHtml(m.participant_id)}

${m.role ? `${escapeHtml(m.role)}` : ""}
`).join(""); return ` ${nav(ctx.token, navOpts(ctx))}
${sectionHeader("Project settings")}

${escapeHtml(project)}

Back to dashboard

Visibility

Who can view this project's sessions and transcripts.

${sectionHeader("Members")} ${membersError ? `
${escapeHtml(membersError)}
` : ""}
${memberRows || `
No members yet.${isMembers ? "" : " Membership only applies when visibility is set to members only."}
`}

Add member

`; }