@polderlabs/bizar 5.3.1 → 5.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bizar-dash/dist/assets/EnvVarsSection-DhLHZ5ua.js +4 -0
- package/bizar-dash/dist/assets/EnvVarsSection-DhLHZ5ua.js.map +1 -0
- package/bizar-dash/dist/assets/MobileChat-9UpvDP6-.js +1 -0
- package/bizar-dash/dist/assets/{MobileChat-OidrxXR9.js.map → MobileChat-9UpvDP6-.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-BFmN9ZWR.js +1 -0
- package/bizar-dash/dist/assets/MobileSettings-BFmN9ZWR.js.map +1 -0
- package/bizar-dash/dist/assets/Toast-BlcKl_dN.js +1 -0
- package/bizar-dash/dist/assets/Toast-BlcKl_dN.js.map +1 -0
- package/bizar-dash/dist/assets/{icons-Bd8piSb2.js → icons-CMPxNV8t.js} +73 -73
- package/bizar-dash/dist/assets/icons-CMPxNV8t.js.map +1 -0
- package/bizar-dash/dist/assets/main-Dhrvp9Gt.js +16 -0
- package/bizar-dash/dist/assets/main-Dhrvp9Gt.js.map +1 -0
- package/bizar-dash/dist/assets/{main-3G6mOhiK.css → main-O2tZVdHm.css} +1 -1
- package/bizar-dash/dist/assets/mobile-DDY1lquG.js +1 -0
- package/bizar-dash/dist/assets/mobile-DDY1lquG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-layout-BjkoHh1W.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-BjkoHh1W.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-CsZQAswA.css → mobile-layout-CJnZNLy3.css} +1 -1
- package/bizar-dash/dist/assets/useSlashCommands-eFCiWHVt.js +2 -0
- package/bizar-dash/dist/assets/useSlashCommands-eFCiWHVt.js.map +1 -0
- package/bizar-dash/dist/index.html +8 -6
- package/bizar-dash/dist/mobile.html +4 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/web/MobileApp.tsx +18 -20
- package/bizar-dash/src/web/components/MobileBottomNav.tsx +38 -0
- package/bizar-dash/src/web/components/MobileDrawer.tsx +68 -0
- package/bizar-dash/src/web/components/MobileHeader.tsx +50 -0
- package/bizar-dash/src/web/components/MobileLayout.tsx +63 -0
- package/bizar-dash/src/web/components/TaskCard.tsx +69 -0
- package/bizar-dash/src/web/components/TaskCreateSheet.tsx +76 -0
- package/bizar-dash/src/web/main.tsx +1 -0
- package/bizar-dash/src/web/mobile/MobileDoctor.tsx +80 -0
- package/bizar-dash/src/web/mobile/MobileEval.tsx +167 -0
- package/bizar-dash/src/web/mobile/MobileMarketplace.tsx +106 -0
- package/bizar-dash/src/web/mobile/MobileMemory.tsx +108 -0
- package/bizar-dash/src/web/mobile/MobileOverview.tsx +213 -0
- package/bizar-dash/src/web/mobile/MobilePlugins.tsx +71 -0
- package/bizar-dash/src/web/mobile/MobileSettings.tsx +235 -180
- package/bizar-dash/src/web/mobile/MobileTasks.tsx +94 -0
- package/bizar-dash/src/web/mobile.tsx +1 -0
- package/bizar-dash/src/web/styles/main.css +388 -0
- package/bizar-dash/src/web/styles/memory.css +45 -0
- package/bizar-dash/src/web/styles/mobile-layout.css +233 -0
- package/bizar-dash/src/web/styles/mobile.css +180 -0
- package/bizar-dash/src/web/styles/settings.css +156 -0
- package/bizar-dash/src/web/styles/tasks.css +305 -0
- package/bizar-dash/tests/mobile-chat.test.tsx +82 -0
- package/bizar-dash/tests/mobile-layout.test.tsx +332 -0
- package/bizar-dash/tests/mobile-misc.test.tsx +321 -0
- package/bizar-dash/tests/mobile-overview.test.tsx +214 -0
- package/bizar-dash/tests/mobile-settings.test.tsx +181 -0
- package/bizar-dash/tests/mobile-tasks.test.tsx +162 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/MobileChat-OidrxXR9.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-B91gVFii.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-B91gVFii.js.map +0 -1
- package/bizar-dash/dist/assets/icons-Bd8piSb2.js.map +0 -1
- package/bizar-dash/dist/assets/main-6YUZY968.js +0 -19
- package/bizar-dash/dist/assets/main-6YUZY968.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-CSsp30lr.js +0 -2
- package/bizar-dash/dist/assets/mobile-CSsp30lr.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-DtwK7DOV.js +0 -1
- package/bizar-dash/dist/assets/mobile-DtwK7DOV.js.map +0 -1
- package/bizar-dash/dist/assets/useSlashCommands-DEkxEvOO.js +0 -2
- package/bizar-dash/dist/assets/useSlashCommands-DEkxEvOO.js.map +0 -1
|
@@ -13,7 +13,7 @@ import type { Notification, Settings, Snapshot, WsMessage } from './lib/types';
|
|
|
13
13
|
import { applyTheme, applyThemeTokens } from './lib/types';
|
|
14
14
|
import { Ws } from './lib/ws';
|
|
15
15
|
import { MobileBottomNav, type MobileTab } from './mobile/MobileBottomNav';
|
|
16
|
-
import {
|
|
16
|
+
import { MobileLayout, type DrawerTab } from './components/MobileLayout';
|
|
17
17
|
import { MobileActivity } from './mobile/views/MobileActivity';
|
|
18
18
|
import { MobileAgents } from './mobile/views/MobileAgents';
|
|
19
19
|
import { MobileConfig } from './mobile/views/MobileConfig';
|
|
@@ -75,6 +75,7 @@ export function MobileApp() {
|
|
|
75
75
|
const [activeTab, setActiveTab] = useState<MainTabId>('activity');
|
|
76
76
|
const [stack, setStack] = useState<MobileView[]>([]);
|
|
77
77
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
78
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
78
79
|
const [pendingChatTaskId, setPendingChatTaskId] = useState<string | null>(null);
|
|
79
80
|
const [snapshot, setSnapshot] = useState<Snapshot | null>(null);
|
|
80
81
|
const [settings, setSettings] = useState<Settings | null>(null);
|
|
@@ -390,27 +391,24 @@ export function MobileApp() {
|
|
|
390
391
|
}
|
|
391
392
|
|
|
392
393
|
return (
|
|
393
|
-
|
|
394
|
-
<
|
|
395
|
-
activeTab={
|
|
396
|
-
|
|
394
|
+
<>
|
|
395
|
+
<MobileLayout
|
|
396
|
+
activeTab={activeTab}
|
|
397
|
+
onTabChange={handleTabChange}
|
|
398
|
+
tabs={MAIN_TABS}
|
|
399
|
+
fullTabs={MAIN_TABS as DrawerTab[]}
|
|
400
|
+
onExitSettings={() => goToTab('activity')}
|
|
397
401
|
onSearch={() => setSearchOpen(true)}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
{stack.length > 0 && (
|
|
408
|
-
<button type="button" className="mobile-back-btn" onClick={popView} aria-label="Go back">
|
|
409
|
-
← Back
|
|
410
|
-
</button>
|
|
411
|
-
)}
|
|
402
|
+
>
|
|
403
|
+
{stack.length > 0 && (
|
|
404
|
+
<button type="button" className="mobile-back-btn" onClick={popView} aria-label="Go back">
|
|
405
|
+
← Back
|
|
406
|
+
</button>
|
|
407
|
+
)}
|
|
408
|
+
{renderView()}
|
|
409
|
+
</MobileLayout>
|
|
412
410
|
|
|
413
411
|
<MobileSearchModal open={searchOpen} onClose={() => setSearchOpen(false)} onNavigate={handleNavigate} />
|
|
414
|
-
|
|
412
|
+
</>
|
|
415
413
|
);
|
|
416
414
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/web/components/MobileBottomNav.tsx — iOS/Android-style bottom tab bar.
|
|
2
|
+
import type { LucideIcon } from 'lucide-react';
|
|
3
|
+
import { cn } from '../lib/utils';
|
|
4
|
+
|
|
5
|
+
export type MobileTab = {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon: LucideIcon;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
tabs: MobileTab[];
|
|
13
|
+
activeTab: string;
|
|
14
|
+
onTabChange: (id: string) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function MobileBottomNav({ tabs, activeTab, onTabChange }: Props) {
|
|
18
|
+
return (
|
|
19
|
+
<nav className="mobile-bottom-nav" role="tablist" aria-label="Primary navigation">
|
|
20
|
+
{tabs.map((t) => {
|
|
21
|
+
const Icon = t.icon;
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
key={t.id}
|
|
25
|
+
role="tab"
|
|
26
|
+
aria-selected={activeTab === t.id}
|
|
27
|
+
className={cn('mobile-nav-item', activeTab === t.id && 'is-active')}
|
|
28
|
+
onClick={() => onTabChange(t.id)}
|
|
29
|
+
type="button"
|
|
30
|
+
>
|
|
31
|
+
<Icon size={20} />
|
|
32
|
+
<span className="mobile-nav-label">{t.label}</span>
|
|
33
|
+
</button>
|
|
34
|
+
);
|
|
35
|
+
})}
|
|
36
|
+
</nav>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/web/components/MobileDrawer.tsx — slide-in drawer for secondary navigation.
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
import type { LucideIcon } from 'lucide-react';
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
|
|
6
|
+
export type DrawerTab = {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
icon: LucideIcon;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
open: boolean;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
tabs: DrawerTab[];
|
|
16
|
+
activeTab: string;
|
|
17
|
+
onTabChange: (id: string) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function MobileDrawer({ open, onClose, tabs, activeTab, onTabChange }: Props) {
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
{open && (
|
|
24
|
+
<div
|
|
25
|
+
className="mobile-drawer-overlay"
|
|
26
|
+
onClick={onClose}
|
|
27
|
+
aria-hidden="true"
|
|
28
|
+
/>
|
|
29
|
+
)}
|
|
30
|
+
<aside
|
|
31
|
+
className={cn('mobile-drawer', open && 'is-open')}
|
|
32
|
+
role="dialog"
|
|
33
|
+
aria-modal="true"
|
|
34
|
+
aria-label="All views"
|
|
35
|
+
>
|
|
36
|
+
<div className="mobile-drawer-head">
|
|
37
|
+
<h2>All Views</h2>
|
|
38
|
+
<button
|
|
39
|
+
onClick={onClose}
|
|
40
|
+
aria-label="Close menu"
|
|
41
|
+
type="button"
|
|
42
|
+
className="mobile-drawer-close"
|
|
43
|
+
>
|
|
44
|
+
<X size={20} />
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="mobile-drawer-grid">
|
|
48
|
+
{tabs.map((t) => {
|
|
49
|
+
const Icon = t.icon;
|
|
50
|
+
return (
|
|
51
|
+
<button
|
|
52
|
+
key={t.id}
|
|
53
|
+
role="tab"
|
|
54
|
+
aria-selected={activeTab === t.id}
|
|
55
|
+
className={cn('mobile-drawer-item', activeTab === t.id && 'is-active')}
|
|
56
|
+
onClick={() => onTabChange(t.id)}
|
|
57
|
+
type="button"
|
|
58
|
+
>
|
|
59
|
+
<Icon size={22} />
|
|
60
|
+
<span>{t.label}</span>
|
|
61
|
+
</button>
|
|
62
|
+
);
|
|
63
|
+
})}
|
|
64
|
+
</div>
|
|
65
|
+
</aside>
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/web/components/MobileHeader.tsx — sticky top bar with menu, title, and actions.
|
|
2
|
+
import { Menu, Search, X } from 'lucide-react';
|
|
3
|
+
import { cn } from '../lib/utils';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
onMenuClick: () => void;
|
|
7
|
+
scrolled: boolean;
|
|
8
|
+
activeTab: string;
|
|
9
|
+
onSettings?: (() => void) | null;
|
|
10
|
+
onSearch?: () => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function MobileHeader({ onMenuClick, scrolled, activeTab, onSettings, onSearch }: Props) {
|
|
14
|
+
return (
|
|
15
|
+
<header className={cn('mobile-header', scrolled && 'is-scrolled')}>
|
|
16
|
+
<button
|
|
17
|
+
onClick={onMenuClick}
|
|
18
|
+
className="mobile-menu-btn"
|
|
19
|
+
aria-label="Open menu"
|
|
20
|
+
type="button"
|
|
21
|
+
>
|
|
22
|
+
<Menu size={20} />
|
|
23
|
+
</button>
|
|
24
|
+
|
|
25
|
+
<h1 className="mobile-header-title">
|
|
26
|
+
{activeTab === 'settings' ? 'Settings' : 'Bizar'}
|
|
27
|
+
</h1>
|
|
28
|
+
|
|
29
|
+
{onSettings ? (
|
|
30
|
+
<button
|
|
31
|
+
onClick={onSettings}
|
|
32
|
+
className="mobile-settings-exit"
|
|
33
|
+
aria-label="Exit settings"
|
|
34
|
+
type="button"
|
|
35
|
+
>
|
|
36
|
+
<X size={20} />
|
|
37
|
+
</button>
|
|
38
|
+
) : (
|
|
39
|
+
<button
|
|
40
|
+
onClick={onSearch}
|
|
41
|
+
className="mobile-search-btn"
|
|
42
|
+
aria-label="Search"
|
|
43
|
+
type="button"
|
|
44
|
+
>
|
|
45
|
+
<Search size={20} />
|
|
46
|
+
</button>
|
|
47
|
+
)}
|
|
48
|
+
</header>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// src/web/components/MobileLayout.tsx — main mobile shell: header + drawer + content + bottom nav.
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { MobileHeader } from './MobileHeader';
|
|
4
|
+
import { MobileDrawer, type DrawerTab } from './MobileDrawer';
|
|
5
|
+
import { MobileBottomNav, type MobileTab } from './MobileBottomNav';
|
|
6
|
+
|
|
7
|
+
// Re-export DrawerTab so consumers only need to import from MobileLayout.
|
|
8
|
+
export { type DrawerTab } from './MobileDrawer';
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
activeTab: string;
|
|
13
|
+
onTabChange: (id: string) => void;
|
|
14
|
+
tabs: MobileTab[];
|
|
15
|
+
fullTabs: DrawerTab[];
|
|
16
|
+
onExitSettings?: () => void;
|
|
17
|
+
onSearch?: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function MobileLayout({
|
|
21
|
+
children,
|
|
22
|
+
activeTab,
|
|
23
|
+
onTabChange,
|
|
24
|
+
tabs,
|
|
25
|
+
fullTabs,
|
|
26
|
+
onExitSettings,
|
|
27
|
+
onSearch,
|
|
28
|
+
}: Props) {
|
|
29
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
30
|
+
const [scrolled, setScrolled] = useState(false);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="mobile-layout">
|
|
34
|
+
<MobileHeader
|
|
35
|
+
onMenuClick={() => setDrawerOpen(true)}
|
|
36
|
+
scrolled={scrolled}
|
|
37
|
+
activeTab={activeTab}
|
|
38
|
+
onSettings={activeTab === 'settings' ? onExitSettings : null}
|
|
39
|
+
onSearch={onSearch}
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<MobileDrawer
|
|
43
|
+
open={drawerOpen}
|
|
44
|
+
onClose={() => setDrawerOpen(false)}
|
|
45
|
+
tabs={fullTabs}
|
|
46
|
+
activeTab={activeTab}
|
|
47
|
+
onTabChange={(id) => {
|
|
48
|
+
onTabChange(id);
|
|
49
|
+
setDrawerOpen(false);
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
<main
|
|
54
|
+
className="mobile-content"
|
|
55
|
+
onScroll={(e) => setScrolled(e.currentTarget.scrollTop > 10)}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</main>
|
|
59
|
+
|
|
60
|
+
<MobileBottomNav tabs={tabs} activeTab={activeTab} onTabChange={onTabChange} />
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/components/TaskCard.tsx — mobile-friendly task card with expandable details.
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { ChevronRight } from 'lucide-react';
|
|
4
|
+
import { Button } from './Button';
|
|
5
|
+
import { api } from '../lib/api';
|
|
6
|
+
import { cn } from '../lib/utils';
|
|
7
|
+
import type { Task } from '../lib/types';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
task: Task;
|
|
11
|
+
workspaceId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function TaskCard({ task, workspaceId }: Props) {
|
|
15
|
+
const [expanded, setExpanded] = useState(false);
|
|
16
|
+
|
|
17
|
+
const handleTransition = async (to: string) => {
|
|
18
|
+
try {
|
|
19
|
+
await api.post(`/tasks/${encodeURIComponent(task.id)}/transition`, { to });
|
|
20
|
+
} catch {
|
|
21
|
+
// best-effort — parent will refresh
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const handleDelete = async () => {
|
|
26
|
+
if (!confirm('Delete this task?')) return;
|
|
27
|
+
try {
|
|
28
|
+
await api.del(`/tasks/${encodeURIComponent(task.id)}`);
|
|
29
|
+
} catch {
|
|
30
|
+
// best-effort — parent will refresh
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className={cn('task-card-mobile', `is-${task.status === 'queued' ? 'todo' : task.status}`)}>
|
|
36
|
+
<div className="task-card-mobile-head" onClick={() => setExpanded(!expanded)} role="button" tabIndex={0} onKeyDown={(e) => e.key === 'Enter' && setExpanded(!expanded)}>
|
|
37
|
+
<div className={cn('task-card-mobile-status', `is-${task.status === 'queued' ? 'todo' : task.status}`)} />
|
|
38
|
+
<div className="task-card-mobile-body">
|
|
39
|
+
<div className="task-card-mobile-title">{task.title}</div>
|
|
40
|
+
{task.priority && task.priority !== 'normal' && (
|
|
41
|
+
<span className={cn('task-priority-badge', `is-${task.priority}`)}>{task.priority}</span>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
<ChevronRight size={16} className={cn('task-chevron', expanded && 'is-rotated')} />
|
|
45
|
+
</div>
|
|
46
|
+
{expanded && (
|
|
47
|
+
<div className="task-card-mobile-details">
|
|
48
|
+
{task.description && <p className="task-card-mobile-desc">{task.description}</p>}
|
|
49
|
+
<div className="task-card-mobile-meta">
|
|
50
|
+
{task.assignee && <span>@{task.assignee}</span>}
|
|
51
|
+
{task.dueDate && <span>{new Date(task.dueDate).toLocaleDateString()}</span>}
|
|
52
|
+
{task.tags?.map((t) => (
|
|
53
|
+
<span key={t} className="tag">{t}</span>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
<div className="task-card-mobile-actions">
|
|
57
|
+
{task.status !== 'doing' && (
|
|
58
|
+
<Button size="sm" onClick={() => handleTransition('doing')}>Start</Button>
|
|
59
|
+
)}
|
|
60
|
+
{task.status !== 'done' && (
|
|
61
|
+
<Button size="sm" onClick={() => handleTransition('done')}>Done</Button>
|
|
62
|
+
)}
|
|
63
|
+
<Button size="sm" variant="danger" onClick={handleDelete}>Delete</Button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/components/TaskCreateSheet.tsx — bottom sheet for creating tasks.
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Button } from './Button';
|
|
4
|
+
import { api } from '../lib/api';
|
|
5
|
+
import { cn } from '../lib/utils';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
workspaceId?: string;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
onCreated: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function TaskCreateSheet({ workspaceId, onClose, onCreated }: Props) {
|
|
14
|
+
const [title, setTitle] = useState('');
|
|
15
|
+
const [description, setDescription] = useState('');
|
|
16
|
+
const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium');
|
|
17
|
+
const [submitting, setSubmitting] = useState(false);
|
|
18
|
+
|
|
19
|
+
const submit = async () => {
|
|
20
|
+
if (!title.trim()) return;
|
|
21
|
+
setSubmitting(true);
|
|
22
|
+
try {
|
|
23
|
+
const payload: Record<string, unknown> = { title, description, priority };
|
|
24
|
+
if (workspaceId) payload.workspaceId = workspaceId;
|
|
25
|
+
await api.post('/tasks', payload);
|
|
26
|
+
onCreated();
|
|
27
|
+
onClose();
|
|
28
|
+
} catch {
|
|
29
|
+
// best-effort — parent handles refresh via onCreated
|
|
30
|
+
} finally {
|
|
31
|
+
setSubmitting(false);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<div className="sheet-backdrop" onClick={onClose} />
|
|
38
|
+
<div className="sheet-bottom" role="dialog" aria-modal="true" aria-labelledby="sheet-title">
|
|
39
|
+
<div className="sheet-handle" />
|
|
40
|
+
<h2 id="sheet-title" className="sheet-title">New Task</h2>
|
|
41
|
+
<input
|
|
42
|
+
autoFocus
|
|
43
|
+
value={title}
|
|
44
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
45
|
+
placeholder="Task title..."
|
|
46
|
+
className="sheet-input"
|
|
47
|
+
/>
|
|
48
|
+
<textarea
|
|
49
|
+
value={description}
|
|
50
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
51
|
+
placeholder="Description (optional)..."
|
|
52
|
+
className="sheet-textarea"
|
|
53
|
+
rows={3}
|
|
54
|
+
/>
|
|
55
|
+
<div className="sheet-priority">
|
|
56
|
+
{(['low', 'medium', 'high'] as const).map((p) => (
|
|
57
|
+
<button
|
|
58
|
+
key={p}
|
|
59
|
+
type="button"
|
|
60
|
+
className={cn('sheet-priority-btn', priority === p && 'is-active', `is-${p}`)}
|
|
61
|
+
onClick={() => setPriority(p)}
|
|
62
|
+
>
|
|
63
|
+
{p}
|
|
64
|
+
</button>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
<div className="sheet-actions">
|
|
68
|
+
<Button onClick={onClose} disabled={submitting}>Cancel</Button>
|
|
69
|
+
<Button variant="primary" onClick={submit} disabled={submitting || !title.trim()}>
|
|
70
|
+
{submitting ? 'Creating...' : 'Create'}
|
|
71
|
+
</Button>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -12,6 +12,7 @@ setLocale('en', en);
|
|
|
12
12
|
// from index.html (e.g. when the auto-redirect to /m is bypassed).
|
|
13
13
|
import './styles/main.css';
|
|
14
14
|
import './styles/mobile.css';
|
|
15
|
+
import './styles/mobile-layout.css';
|
|
15
16
|
import './styles/glyphs.css';
|
|
16
17
|
import './styles/minimax-usage.css';
|
|
17
18
|
import './styles/skills.css';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/web/mobile/MobileDoctor.tsx — v5.4 mobile doctor health view with 30s auto-refresh.
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { CheckCircle2, AlertTriangle, XCircle } from 'lucide-react';
|
|
4
|
+
import { api } from '../lib/api';
|
|
5
|
+
import type { DoctorStatus, DoctorCheck } from '../lib/types';
|
|
6
|
+
|
|
7
|
+
type HealthResponse = {
|
|
8
|
+
status: DoctorStatus;
|
|
9
|
+
issues: DoctorCheck[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const REFRESH_INTERVAL_MS = 30_000;
|
|
13
|
+
|
|
14
|
+
export function MobileDoctor() {
|
|
15
|
+
const [data, setData] = useState<HealthResponse | null>(null);
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const fetch_ = () => {
|
|
20
|
+
api.get<HealthResponse>('/doctor/health')
|
|
21
|
+
.then((r) => {
|
|
22
|
+
setData(r);
|
|
23
|
+
setLoading(false);
|
|
24
|
+
})
|
|
25
|
+
.catch(() => setLoading(false));
|
|
26
|
+
};
|
|
27
|
+
fetch_();
|
|
28
|
+
const id = setInterval(fetch_, REFRESH_INTERVAL_MS);
|
|
29
|
+
return () => clearInterval(id);
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
if (loading) {
|
|
33
|
+
return (
|
|
34
|
+
<div className="mobile-loading">
|
|
35
|
+
<p>Running health checks…</p>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const status: DoctorStatus = data?.status ?? 'fail';
|
|
41
|
+
const issues: DoctorCheck[] = data?.issues ?? [];
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="mobile-doctor">
|
|
45
|
+
<div className={`mobile-doctor-status is-${status}`}>
|
|
46
|
+
{status === 'ok' && <CheckCircle2 size={48} />}
|
|
47
|
+
{status === 'warn' && <AlertTriangle size={48} />}
|
|
48
|
+
{status === 'fail' && <XCircle size={48} />}
|
|
49
|
+
<div className="mobile-doctor-status-text">{status.toUpperCase()}</div>
|
|
50
|
+
<div className="mobile-doctor-status-sub">Auto-refreshes every 30s</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{issues.length > 0 && (
|
|
54
|
+
<div className="mobile-doctor-issues">
|
|
55
|
+
<h3>Issues ({issues.length})</h3>
|
|
56
|
+
{issues.map((i, idx) => (
|
|
57
|
+
<div key={idx} className={`mobile-doctor-issue is-${i.status}`}>
|
|
58
|
+
{i.status === 'warn' ? (
|
|
59
|
+
<AlertTriangle size={16} />
|
|
60
|
+
) : (
|
|
61
|
+
<XCircle size={16} />
|
|
62
|
+
)}
|
|
63
|
+
<div>
|
|
64
|
+
<div className="mobile-doctor-issue-name">{i.name}</div>
|
|
65
|
+
<div className="mobile-doctor-issue-msg">{i.message}</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{status === 'ok' && issues.length === 0 && (
|
|
73
|
+
<div className="mobile-doctor-ok">
|
|
74
|
+
<CheckCircle2 size={24} />
|
|
75
|
+
<p>All systems healthy.</p>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// src/web/mobile/MobileEval.tsx — v5.4 mobile eval view with runs and schedules tabs.
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { ClipboardCheck, Clock } from 'lucide-react';
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
import { api } from '../lib/api';
|
|
6
|
+
import type { EvalRunSummary } from '../components/EvalRunCard';
|
|
7
|
+
|
|
8
|
+
type EvalSchedule = {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
suitePath: string;
|
|
12
|
+
cron: string;
|
|
13
|
+
agent: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function pickStatus(run: EvalRunSummary): 'pass' | 'warn' | 'fail' {
|
|
17
|
+
if (run.total <= 0) return 'fail';
|
|
18
|
+
if (run.failed === 0) return 'pass';
|
|
19
|
+
if (run.passed / run.total > 0.8) return 'warn';
|
|
20
|
+
return 'fail';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function MobileEval() {
|
|
24
|
+
const [runs, setRuns] = useState<EvalRunSummary[]>([]);
|
|
25
|
+
const [schedules, setSchedules] = useState<EvalSchedule[]>([]);
|
|
26
|
+
const [tab, setTab] = useState<'runs' | 'schedules'>('runs');
|
|
27
|
+
const [loadingRuns, setLoadingRuns] = useState(true);
|
|
28
|
+
const [loadingSchedules, setLoadingSchedules] = useState(false);
|
|
29
|
+
const [selectedRun, setSelectedRun] = useState<EvalRunSummary | null>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
api.get<{ runs: EvalRunSummary[] }>('/eval/runs')
|
|
33
|
+
.then((r) => setRuns(r.runs ?? []))
|
|
34
|
+
.catch(() => {/* best-effort */})
|
|
35
|
+
.finally(() => setLoadingRuns(false));
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const loadSchedules = () => {
|
|
39
|
+
setLoadingSchedules(true);
|
|
40
|
+
api.get<{ schedules: EvalSchedule[] }>('/eval/schedules')
|
|
41
|
+
.then((r) => setSchedules(r.schedules ?? []))
|
|
42
|
+
.catch(() => {/* best-effort */})
|
|
43
|
+
.finally(() => setLoadingSchedules(false));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (tab === 'schedules' && schedules.length === 0) {
|
|
48
|
+
loadSchedules();
|
|
49
|
+
}
|
|
50
|
+
}, [tab]);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="mobile-eval">
|
|
54
|
+
<div className="mobile-eval-tabs">
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
className={cn('mobile-eval-tab', tab === 'runs' && 'is-active')}
|
|
58
|
+
onClick={() => setTab('runs')}
|
|
59
|
+
>
|
|
60
|
+
<ClipboardCheck size={12} aria-hidden />
|
|
61
|
+
Runs
|
|
62
|
+
</button>
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
className={cn('mobile-eval-tab', tab === 'schedules' && 'is-active')}
|
|
66
|
+
onClick={() => setTab('schedules')}
|
|
67
|
+
>
|
|
68
|
+
<Clock size={12} aria-hidden />
|
|
69
|
+
Schedules
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{tab === 'runs' && (
|
|
74
|
+
<div className="mobile-eval-runs">
|
|
75
|
+
{loadingRuns ? (
|
|
76
|
+
<div className="mobile-loading"><p>Loading runs…</p></div>
|
|
77
|
+
) : runs.length === 0 ? (
|
|
78
|
+
<div className="mobile-empty">
|
|
79
|
+
<ClipboardCheck size={32} />
|
|
80
|
+
<p>No eval runs yet.</p>
|
|
81
|
+
</div>
|
|
82
|
+
) : (
|
|
83
|
+
runs.map((r) => {
|
|
84
|
+
const status = pickStatus(r);
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
key={r.id}
|
|
88
|
+
className="mobile-eval-run-card"
|
|
89
|
+
onClick={() => setSelectedRun(r)}
|
|
90
|
+
role="button"
|
|
91
|
+
tabIndex={0}
|
|
92
|
+
onKeyDown={(e) => e.key === 'Enter' && setSelectedRun(r)}
|
|
93
|
+
>
|
|
94
|
+
<div className={cn('mobile-eval-status', `is-${status}`)}>
|
|
95
|
+
{r.passed}/{r.total}
|
|
96
|
+
</div>
|
|
97
|
+
<div className="mobile-eval-run-info">
|
|
98
|
+
<div className="mobile-eval-run-id">{r.id}</div>
|
|
99
|
+
<div className="mobile-eval-run-time">
|
|
100
|
+
{new Date(r.startedAt).toLocaleString()}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
})
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{tab === 'schedules' && (
|
|
111
|
+
<div className="mobile-eval-schedules">
|
|
112
|
+
{loadingSchedules ? (
|
|
113
|
+
<div className="mobile-loading"><p>Loading schedules…</p></div>
|
|
114
|
+
) : schedules.length === 0 ? (
|
|
115
|
+
<div className="mobile-empty">
|
|
116
|
+
<Clock size={32} />
|
|
117
|
+
<p>No eval schedules.</p>
|
|
118
|
+
</div>
|
|
119
|
+
) : (
|
|
120
|
+
schedules.map((s) => (
|
|
121
|
+
<div key={s.id} className="mobile-eval-schedule-card">
|
|
122
|
+
<div className="mobile-eval-schedule-name">{s.name}</div>
|
|
123
|
+
<div className="mobile-eval-schedule-meta">
|
|
124
|
+
{s.cron} · {s.suitePath} · agent:{s.agent || 'thor'}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
))
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{selectedRun && (
|
|
133
|
+
<div className="mobile-eval-run-sheet">
|
|
134
|
+
<div className="mobile-bottom-sheet-header">
|
|
135
|
+
<h3>Run {selectedRun.id}</h3>
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
className="mobile-bottom-sheet-close"
|
|
139
|
+
onClick={() => setSelectedRun(null)}
|
|
140
|
+
aria-label="Close"
|
|
141
|
+
>
|
|
142
|
+
×
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
<div className="mobile-eval-run-sheet-body">
|
|
146
|
+
<div className="mobile-eval-run-sheet-row">
|
|
147
|
+
<span>Suite</span>
|
|
148
|
+
<span>{selectedRun.suitePath}</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="mobile-eval-run-sheet-row">
|
|
151
|
+
<span>Passed</span>
|
|
152
|
+
<span>{selectedRun.passed}</span>
|
|
153
|
+
</div>
|
|
154
|
+
<div className="mobile-eval-run-sheet-row">
|
|
155
|
+
<span>Failed</span>
|
|
156
|
+
<span>{selectedRun.failed}</span>
|
|
157
|
+
</div>
|
|
158
|
+
<div className="mobile-eval-run-sheet-row">
|
|
159
|
+
<span>Started</span>
|
|
160
|
+
<span>{new Date(selectedRun.startedAt).toLocaleString()}</span>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|