>();
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 = `
`;
return `
Prompts captured
${total} last 14d
Team
By user
${teamView}
${userView}
`;
}
// --- 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)}
`;
}
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 `
`;
}).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 ``;
}
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), {}, `★ Starred `)
: annotationForm(addAction, { event_id: e.id, kind: "star" }, `☆ Star `);
const tagChips = tags
.map((t) =>
annotationForm(deleteAction(t.id), {}, `#${escapeHtml(t.value ?? "")} × `)
)
.join("");
const tagAdder = `
+ Tag
`;
const decisionControl = decision
? annotationForm(deleteAction(decision.id), {}, `⚑ Decision × `)
: annotationForm(addAction, { event_id: e.id, kind: "decision", value: eventExcerpt(e) }, `⚑ Decision `);
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")}
`;
}
// --- 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) => `${s === "" ? "Any source" : 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))}
`;
}
// --- 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))}
`;
}
// --- 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)} ` : ""}
Remove
`).join("");
return `
${nav(ctx.token, navOpts(ctx))}
${sectionHeader("Members")}
${membersError ? `
${escapeHtml(membersError)}
` : ""}
${memberRows || `
No members yet.${isMembers ? "" : " Membership only applies when visibility is set to members only."}
`}
Add member
Add
`;
}