-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathCreatureDetail.tsx
More file actions
136 lines (124 loc) · 7.25 KB
/
Copy pathCreatureDetail.tsx
File metadata and controls
136 lines (124 loc) · 7.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import { useState, useRef, useEffect } from 'react';
import { useStore } from '@/state';
import * as api from '@/api';
import { Event } from './Event';
import { BudgetInfo } from './BudgetInfo';
import { MindContent } from './MindContent';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { RefreshCw } from 'lucide-react';
function MessageBar() {
const [text, setText] = useState('');
const name = useStore(s => s.selected);
const send = async () => {
if (!name || !text.trim()) return;
await api.sendMessage(name, text.trim());
setText('');
};
return (
<div className="sticky bottom-0 bg-bg px-4 py-3 border-t border-border-light flex gap-2">
<textarea
className="flex-1 bg-white border border-input text-text-primary px-3 py-2 rounded font-sans text-[13px] resize-y min-h-[38px] max-h-[200px] focus:outline-none focus:border-ring"
placeholder="Message to creature... (Cmd+Enter to send)"
rows={2}
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && e.metaKey) { e.preventDefault(); send(); }
}}
/>
<button className="bg-[#eff6ff] border border-accent-blue text-accent-blue px-4 py-2 rounded cursor-pointer font-sans text-[13px] hover:bg-[#dbeafe] transition-colors" onClick={send}>Send</button>
</div>
);
}
export function CreatureDetail() {
const name = useStore(s => s.selected)!;
const c = useStore(s => s.creatures[name]);
const events = useStore(s => s.creatureEvents);
const mind = useStore(s => s.mindData);
const tab = useStore(s => s.selectedTab);
const setSelectedTab = useStore(s => s.setSelectedTab);
const selectCreature = useStore(s => s.selectCreature);
const loadMind = useStore(s => s.loadMind);
const refresh = useStore(s => s.refresh);
const degraded = useStore(s => s.health.status !== 'healthy');
const eventsEndRef = useRef<HTMLDivElement>(null);
const [busy, setBusy] = useState<string | null>(null);
const doAction = async (action: string, needsConfirm?: string) => {
if (needsConfirm && !confirm(needsConfirm)) return;
setBusy(action);
try {
await api.creatureAction(name, action as any);
refresh();
} finally {
setBusy(null);
}
};
useEffect(() => {
if (tab === 'log') {
eventsEndRef.current?.scrollIntoView();
}
}, [events.length, tab]);
const genomeTabs = mind?.tabs || [];
const tabIds = ['log', ...genomeTabs.map(t => t.id)];
const tabLabels: Record<string, string> = { log: 'log' };
genomeTabs.forEach(t => { tabLabels[t.id] = t.label || t.id; });
const sr = c?.sleepReason === 'budget' ? ' (budget cap)' : '';
return (
<>
{/* Header */}
<div className="sticky top-0 z-20 bg-bg/95 backdrop-blur-sm px-4 py-3 border-b border-border-light flex items-center gap-3">
<h2 className="text-sm font-semibold">{name}</h2>
<div className="text-xs text-text-secondary">
{c?.model && <><span className="text-text-muted text-[11px]">{c.model}</span> · </>}
{c?.status}{sr}
{c?.janeeVersion && <> · <span className="text-text-muted text-[11px]">janee {c.janeeVersion}</span></>}
</div>
<BudgetInfo />
<button className="bg-white border border-[#d0d0d0] text-text-secondary px-1.5 py-0.5 rounded text-[11px] cursor-pointer hover:bg-[#f5f5f5] hover:text-text-primary transition-colors disabled:opacity-40 disabled:cursor-not-allowed" disabled={!!busy} onClick={() => doAction('wake')}>{busy === 'wake' ? 'waking...' : 'wake'}</button>
<button className="bg-white border border-[#d0d0d0] text-text-secondary px-1.5 py-0.5 rounded text-[11px] cursor-pointer hover:bg-[#f5f5f5] hover:text-text-primary transition-colors disabled:opacity-40 disabled:cursor-not-allowed" disabled={degraded || !!busy} title={degraded ? 'Orchestrator degraded' : undefined} onClick={() => doAction('restart')}>{busy === 'restart' ? 'restarting...' : 'restart'}</button>
<button className="bg-white border border-warn text-warn-light px-1.5 py-0.5 rounded text-[11px] cursor-pointer hover:bg-[#fffbf5] transition-colors disabled:opacity-40 disabled:cursor-not-allowed" disabled={degraded || !!busy} title={degraded ? 'Orchestrator degraded' : undefined} onClick={() => doAction('rebuild', `Rebuild "${name}"? This destroys the container writable layer.`)}>{busy === 'rebuild' ? 'rebuilding...' : 'rebuild'}</button>
<button className="bg-white border border-[#d0d0d0] text-text-secondary px-1.5 py-0.5 rounded text-[11px] cursor-pointer hover:bg-[#f5f5f5] hover:text-text-primary transition-colors disabled:opacity-40 disabled:cursor-not-allowed" disabled={degraded || !!busy} title={degraded ? 'Orchestrator degraded' : 'Recreate container preserving installed packages'} onClick={() => doAction('remount', `Remount "${name}"? Preserves installed packages but restarts the container.`)}>{busy === 'remount' ? 'remounting...' : 'remount'}</button>
<button className="bg-white border border-warn text-warn-light px-1.5 py-0.5 rounded text-[11px] cursor-pointer hover:bg-[#fffbf5] transition-colors disabled:opacity-40 disabled:cursor-not-allowed" disabled={!!busy} onClick={async () => {
if (!confirm(`Archive creature "${name}"? It will be stopped and moved to the archive.`)) return;
setBusy('archive');
try { await api.creatureAction(name, 'archive'); refresh(); selectCreature(null); } finally { setBusy(null); }
}}>{busy === 'archive' ? 'archiving...' : 'archive'}</button>
</div>
{/* Tab bar */}
<div className="sticky top-[44px] z-[15] bg-bg border-b border-border-light px-4">
<Tabs value={tab} onValueChange={setSelectedTab}>
<div className="flex items-center">
<TabsList className="bg-transparent h-auto p-0 gap-0">
{tabIds.map(id => (
<TabsTrigger
key={id}
value={id}
className="rounded-none border-0 border-b-2 border-transparent data-[state=active]:border-accent-blue data-[state=active]:text-accent-blue data-[state=active]:shadow-none data-[state=active]:bg-transparent dark:data-[state=active]:bg-transparent dark:data-[state=active]:border-transparent dark:data-[state=active]:border-b-accent-blue bg-transparent px-4 py-2 text-xs text-text-muted hover:text-text-secondary"
>
{tabLabels[id] || id}
</TabsTrigger>
))}
</TabsList>
<button className="ml-auto px-3 py-2 cursor-pointer text-text-muted text-xs hover:text-text-secondary bg-transparent border-none transition-colors" onClick={() => loadMind()}>
<RefreshCw className="size-3" />
</button>
</div>
</Tabs>
</div>
{/* Content */}
<div className="p-4 flex-1">
{tab === 'log' ? (
<div className="flex flex-col gap-1 font-mono">
{events.map((ev, i) => <Event key={(ev.t || '') + i} ev={ev} />)}
<div ref={eventsEndRef} />
</div>
) : mind ? (
<MindContent mindData={mind} tabId={tab} />
) : (
<div className="font-mono text-xs text-text-primary">Loading...</div>
)}
</div>
{tab === 'log' && <MessageBar />}
</>
);
}