Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added desktop/assets/octo-idle-sprite.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added desktop/assets/octo-thinking-sprite.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
123 changes: 91 additions & 32 deletions desktop/src/renderer/src/components/DashboardScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { AlertTriangle, Clock, Download, ExternalLink, Eye, FileJson, GitBranch, ListChecks, Pencil, Play, Plus, RotateCw, Square, Trash2, Wrench, X } from "lucide-react";
import { motion } from "framer-motion";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ReactNode } from "react";

import octoIdleSprite from "../../../../assets/octo-idle-sprite.png";
import octoThinkingSprite from "../../../../assets/octo-thinking-sprite.png";
import octoImage from "../../../../assets/octo.png";
import type { CopyFn } from "../lib/appTypes";
import { Button } from "./Button";
Expand Down Expand Up @@ -128,6 +131,24 @@ function statusTextClass(status?: string): string {
return "worker-detail-status";
}

function isIdleOctoState(status?: string): boolean {
return String(status ?? "").toLowerCase() === "idle";
}

function isThinkingOctoState(status?: string): boolean {
return String(status ?? "").toLowerCase() === "thinking";
}

function animatedOctoForState(status?: string): { className: string; sprite: string } | null {
if (isIdleOctoState(status)) {
return { className: "dashboard-octo-idle", sprite: octoIdleSprite };
}
if (isThinkingOctoState(status)) {
return { className: "dashboard-octo-thinking", sprite: octoThinkingSprite };
}
return null;
}

function formatTime(value?: string | number): string {
if (!value) {
return "-";
Expand Down Expand Up @@ -467,6 +488,7 @@ export function DashboardScreen({
const selectedWorkerTemplate = selectedWorker?.template_id
? templates.find((template) => template.id === selectedWorker.template_id) ?? null
: null;
const animatedOcto = animatedOctoForState(displayOctoState);

function startCreateTemplate(): void {
setEditingTemplateId("");
Expand Down Expand Up @@ -545,29 +567,68 @@ export function DashboardScreen({
void window.octopalDesktop.openOctopalLogs(installDir);
}

function renderControl() {
function renderDashboardHeader({
title,
titleRaw = title,
detail,
detailRaw = detail,
latest,
latestRaw = latest,
actions,
}: {
title: string;
titleRaw?: string;
detail: string;
detailRaw?: string;
latest?: string;
latestRaw?: string;
actions?: ReactNode;
}) {
return (
<section className="dashboard-control">
<div className="dashboard-assistant-head">
<div className="dashboard-octo-stack">
<div className="dashboard-assistant-head">
<div className="dashboard-octo-stack">
{animatedOcto ? (
<span
className={`octo dashboard-octo ${animatedOcto.className}`}
role="img"
aria-label="Octopal mascot"
style={{ backgroundImage: `url(${animatedOcto.sprite})` }}
/>
) : (
<img className="octo dashboard-octo" src={octoImage} alt="Octopal mascot" />
<span className={statusClass(displayOctoState)}>{displayOctoState}</span>
</div>
<div className="dashboard-bubble">
<h1 title={octoHeadlineRaw}>{octoHeadline}</h1>
<p className="dashboard-octo-detail" title={octoDetailRaw}>{octoDetail}</p>
<p className="dashboard-latest" title={latestActionRaw}>
<strong>{copy("latestAction")}:</strong> {latestAction}
)}
<span className={statusClass(displayOctoState)}>{displayOctoState}</span>
</div>
<div className="dashboard-bubble">
<h1 title={titleRaw}>{title}</h1>
<p className="dashboard-octo-detail" title={detailRaw}>{detail}</p>
{latest ? (
<p className="dashboard-latest" title={latestRaw}>
<strong>{copy("latestAction")}:</strong> {latest}
</p>
</div>
{updateAvailable || desktopUpdateAvailable ? (
<div className="dashboard-actions">
<Button type="button" variant="ghost" onClick={() => setView("system")}>
{copy("updateReady")}
</Button>
</div>
) : null}
</div>
{actions ? <div className="dashboard-actions">{actions}</div> : null}
</div>
);
}

function renderControl() {
return (
<section className="dashboard-control">
{renderDashboardHeader({
title: octoHeadline,
titleRaw: octoHeadlineRaw,
detail: octoDetail,
detailRaw: octoDetailRaw,
latest: latestAction,
latestRaw: latestActionRaw,
actions: updateAvailable || desktopUpdateAvailable ? (
<Button type="button" variant="ghost" onClick={() => setView("system")}>
{copy("updateReady")}
</Button>
) : null,
})}

{attention || octoNeedsAttention || dashboardError ? (
<div className="dashboard-attention-panel" role="alert">
Expand Down Expand Up @@ -672,13 +733,12 @@ export function DashboardScreen({
function renderWorkers() {
return (
<section className="dashboard-workers-view">
<div className="dashboard-assistant-head dashboard-assistant-head-compact">
<img className="octo dashboard-octo-small" src={octoImage} alt="Octopal mascot" />
<div className="dashboard-bubble">
<h1>{copy("workerTemplates")}</h1>
<p>{copy("workerTemplatesBody")}</p>
</div>
</div>
{renderDashboardHeader({
title: copy("workerTemplates"),
detail: copy("workerTemplatesBody"),
latest: latestAction,
latestRaw: latestActionRaw,
})}
{templateError ? <p className="dashboard-inline-error">{templateError}</p> : null}
{templateNotice ? <p className="dashboard-inline-notice">{templateNotice}</p> : null}
<div className="worker-studio-grid">
Expand Down Expand Up @@ -723,13 +783,12 @@ export function DashboardScreen({
function renderSystem() {
return (
<section className="dashboard-system-view">
<div className="dashboard-assistant-head dashboard-assistant-head-compact">
<img className="octo dashboard-octo-small" src={octoImage} alt="Octopal mascot" />
<div className="dashboard-bubble">
<h1 title={systemTitle}>{systemTitle}</h1>
<p className="dashboard-octo-detail" title={systemDetail}>{systemDetail}</p>
</div>
</div>
{renderDashboardHeader({
title: systemTitle,
detail: systemDetail,
latest: latestAction,
latestRaw: latestActionRaw,
})}
{attention || dashboardError ? (
<div className="dashboard-attention-panel" role="alert">
<AlertTriangle />
Expand Down
41 changes: 26 additions & 15 deletions desktop/src/renderer/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2093,29 +2093,31 @@ p {
padding-right: 210px;
}

.dashboard-assistant-head-compact {
grid-template-columns: 76px minmax(0, 1fr);
padding-right: 220px;
}

.dashboard-octo {
width: 92px;
height: 92px;
object-fit: contain;
}

.dashboard-octo-idle,
.dashboard-octo-thinking {
display: block;
background-repeat: no-repeat;
background-position: 0 0;
background-size: 1500% 100%;
animation: octo-sprite-frames 4.8s steps(15) infinite;
}

.dashboard-octo-thinking {
animation-duration: 9.6s;
}

.dashboard-octo-stack {
display: grid;
justify-items: center;
gap: 6px;
}

.dashboard-octo-small {
width: 68px;
height: 68px;
object-fit: contain;
}

.dashboard-bubble {
position: relative;
width: fit-content;
Expand Down Expand Up @@ -3263,6 +3265,12 @@ p {
}
}

@keyframes octo-sprite-frames {
to {
background-position-x: 107.142857%;
}
}

@media (max-width: 860px) {
.titlebar {
height: 52px;
Expand Down Expand Up @@ -3470,15 +3478,13 @@ p {
flex: 1 0 auto;
}

.dashboard-assistant-head,
.dashboard-assistant-head-compact {
.dashboard-assistant-head {
grid-template-columns: 76px minmax(0, 1fr);
gap: 14px;
padding-right: 0;
}

.dashboard-octo,
.dashboard-octo-small {
.dashboard-octo {
width: 68px;
height: 68px;
}
Expand Down Expand Up @@ -3563,4 +3569,9 @@ p {
scroll-behavior: auto !important;
transition-duration: 1ms !important;
}

.dashboard-octo-idle,
.dashboard-octo-thinking {
animation: none !important;
}
}