@crievo/omni 0.1.6 → 0.1.8
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/omnivideo.config.example.json +1 -0
- package/package.json +1 -1
- package/reference/cloud.md +15 -1
- package/reference/learnings.md +7 -0
- package/reference/studio.md +1 -0
- package/studio/api/app.ts +4 -0
- package/studio/api/routes/cost.ts +13 -0
- package/studio/api/routes/me.ts +8 -0
- package/studio/api/services/agent-tools.ts +2 -2
- package/studio/api/services/cost.ts +22 -0
- package/studio/api/services/me.ts +67 -0
- package/studio/api/services/sessions.ts +8 -2
- package/studio/runner/runner.ts +57 -10
- package/studio/shared/omni-screens.ts +1 -1
- package/studio/shared/schemas/cost.ts +10 -0
- package/studio/shared/types/api.ts +74 -0
- package/studio/studio.ts +27 -2
- package/studio/web/components/AppShell.tsx +11 -3
- package/studio/web/components/FirstConnection.tsx +222 -0
- package/studio/web/components/OfflineNotice.tsx +59 -0
- package/studio/web/components/SetupSteps.tsx +63 -0
- package/studio/web/components/Sidebar.tsx +10 -25
- package/studio/web/components/StudioOffline.tsx +48 -50
- package/studio/web/components/UserCard.tsx +79 -0
- package/studio/web/components/icons.tsx +2 -0
- package/studio/web/hooks/useComputers.ts +55 -0
- package/studio/web/hooks/useCost.ts +11 -0
- package/studio/web/hooks/useMe.ts +32 -0
- package/studio/web/hooks/useSetupProgress.ts +54 -0
- package/studio/web/lib/me.ts +17 -0
- package/studio/web/lib/query-client.ts +6 -0
- package/studio/web/lib/settings-categories.ts +2 -2
- package/studio/web/lib/setup-retry.ts +29 -0
- package/studio/web/lib/studio-offline.ts +105 -59
- package/studio/web/pages/ConnectPreviewPage/index.tsx +215 -0
- package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +18 -9
- package/studio/web/pages/SettingsPage/components/AgentCostPanel.tsx +253 -0
- package/studio/web/pages/SettingsPage/components/ComputersPanel.tsx +145 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +2 -2
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +3 -3
- package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +7 -2
- package/studio/web/pages/SettingsPage/index.tsx +15 -1
- package/studio/web/pages/VideoPage/components/PublishForm.tsx +5 -2
- package/studio/web/router.tsx +8 -0
- package/studio/web/styles/components/setup-steps.css +163 -0
- package/studio/web/styles/components/user-card.css +121 -0
- package/studio/web/styles/index.css +2 -0
- package/studio/web/styles/pages/settings/shell.css +79 -0
- package/studio/web/styles/pages/settings/usage.css +47 -0
- package/studio/web/styles/pages/studio-offline.css +224 -38
- package/tools/cost.mjs +114 -0
- package/tools/lib/agents.mjs +9 -3
- package/tools/lib/channel.mjs +1 -1
- package/tools/lib/cloud-user.mjs +58 -0
- package/tools/lib/cost.mjs +357 -0
- package/tools/lib/deps.mjs +27 -12
- package/tools/lib/oauth-client.mjs +28 -14
- package/tools/lib/pages-style.mjs +67 -0
- package/tools/lib/pairing.mjs +1 -1
- package/tools/lib/schedule/schedule.mjs +1 -1
- package/tools/lib/self-update.mjs +122 -0
- package/tools/lib/setup-progress.mjs +315 -0
- package/tools/lib/up.mjs +176 -61
- package/tools/metrics.mjs +6 -1
- package/tools/omni.mjs +41 -16
- package/tools/omnichat.mjs +45 -9
- package/tools/platforms.mjs +75 -14
- package/tools/runner.mjs +6 -6
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
/* Configurações → Agente de IA: the agent accounts (Claude and Codex), Codex's options and the model per stage
|
|
1
|
+
/* Configurações → Agente de IA: the agent accounts (Claude and Codex), Codex's options and the model per stage and,
|
|
2
|
+
in the hosted studio, the computers signed in and the actions that went through to them (ComputersPanel) */
|
|
2
3
|
|
|
3
4
|
import type { Settings } from "../../../types/api";
|
|
4
5
|
import { Panel } from "../../../components/ui/panel";
|
|
5
6
|
import { AgentAccounts, CodexSettings } from "./AgentAccounts";
|
|
7
|
+
import { useRunner } from "../../../hooks/useRunner";
|
|
8
|
+
import { ComputersPanel } from "./ComputersPanel";
|
|
6
9
|
import { ModelsSettings } from "./ModelsSettings";
|
|
7
10
|
|
|
8
|
-
export function AgentCategory({ settings, reload }: { settings: Settings; reload: () => void }) {
|
|
11
|
+
export function AgentCategory({ settings, reload }: { settings: Settings | null; reload: () => void }) {
|
|
12
|
+
const { hosted } = useRunner();
|
|
9
13
|
return (
|
|
10
14
|
<>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
{settings && (
|
|
16
|
+
<>
|
|
17
|
+
<Panel title="Contas de agente">
|
|
18
|
+
<AgentAccounts agents={settings.agents} onSave={reload} />
|
|
19
|
+
<CodexSettings codex={settings.agents.codex} onSave={reload} />
|
|
20
|
+
</Panel>
|
|
21
|
+
<Panel title="Modelos do Claude">
|
|
22
|
+
<ModelsSettings models={settings.models} onSave={reload} />
|
|
23
|
+
</Panel>
|
|
24
|
+
</>
|
|
25
|
+
)}
|
|
26
|
+
{hosted && <ComputersPanel />}
|
|
18
27
|
</>
|
|
19
28
|
);
|
|
20
29
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/* What the agents spent (Configurações → Uso e custo): the Claude sessions' cost by video, stage
|
|
2
|
+
and model. The accounting is tools/lib/cost.mjs (terminal: tools/cost.mjs). */
|
|
3
|
+
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { Link } from "@tanstack/react-router";
|
|
6
|
+
import { useCost } from "../../../hooks/useCost";
|
|
7
|
+
import type { AgentCost } from "../../../types/api";
|
|
8
|
+
import { Panel } from "../../../components/ui/panel";
|
|
9
|
+
import { Skeleton } from "../../../components/ui/skeleton";
|
|
10
|
+
|
|
11
|
+
const STAGE_LABEL: Record<string, string> = {
|
|
12
|
+
orchestrator: "Orquestrador",
|
|
13
|
+
"av-script": "Roteiro",
|
|
14
|
+
"av-motion": "Motion (roteiro e base)",
|
|
15
|
+
compose: "Composição (um fork por cena)",
|
|
16
|
+
wireframe: "Wireframes (um fork por cena)",
|
|
17
|
+
"av-narration": "Narração",
|
|
18
|
+
"av-voice": "Voz",
|
|
19
|
+
"av-render": "Render",
|
|
20
|
+
"av-captions": "Legendas",
|
|
21
|
+
"av-assemble": "Montagem",
|
|
22
|
+
"av-editor": "Editor",
|
|
23
|
+
"av-reference": "Estilo visual",
|
|
24
|
+
"av-thumbnail": "Thumbnail",
|
|
25
|
+
"av-preview": "Prévia (aposentada)",
|
|
26
|
+
other: "Outros",
|
|
27
|
+
};
|
|
28
|
+
const KIND_LABEL: Record<string, string> = {
|
|
29
|
+
video: "Vídeos",
|
|
30
|
+
edit: "Editor",
|
|
31
|
+
reference: "Estilos visuais",
|
|
32
|
+
analysis: "Análise",
|
|
33
|
+
omni: "Omni",
|
|
34
|
+
};
|
|
35
|
+
const PERIODS: { days: number | null; label: string }[] = [
|
|
36
|
+
{ days: 7, label: "7 dias" },
|
|
37
|
+
{ days: 30, label: "30 dias" },
|
|
38
|
+
{ days: null, label: "Tudo" },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const usd = (v: number) => `US$ ${v.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
42
|
+
const pct = (v: number, total: number) => (total > 0 ? `${Math.round((v / total) * 100)}%` : "—");
|
|
43
|
+
const sorted = (o: Record<string, number>) => Object.entries(o).sort((a, b) => b[1] - a[1]);
|
|
44
|
+
const dayLabel = (d: string) =>
|
|
45
|
+
new Date(`${d}T12:00:00`).toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
|
|
46
|
+
|
|
47
|
+
/* one row per entry: the name, a bar relative to the largest, the value and its share */
|
|
48
|
+
function Bars({ entries, total, label }: { entries: [string, number][]; total: number; label: (k: string) => string }) {
|
|
49
|
+
const max = Math.max(...entries.map(e => e[1]), 0);
|
|
50
|
+
return (
|
|
51
|
+
<ul className="cost-rows">
|
|
52
|
+
{entries.map(([k, v]) => (
|
|
53
|
+
<li key={k}>
|
|
54
|
+
<span className="cost-name">{label(k)}</span>
|
|
55
|
+
<div className="bar">
|
|
56
|
+
<div style={{ width: `${max ? Math.max((v / max) * 100, v ? 1 : 0) : 0}%` }} />
|
|
57
|
+
</div>
|
|
58
|
+
<span className="cost-value">{usd(v)}</span>
|
|
59
|
+
<span className="subtle cost-pct">{pct(v, total)}</span>
|
|
60
|
+
</li>
|
|
61
|
+
))}
|
|
62
|
+
</ul>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Summary({ c }: { c: AgentCost }) {
|
|
67
|
+
const videos = c.by_video.filter(v => v.type === "video" || v.type === "edit");
|
|
68
|
+
const perVideo = videos.length ? videos.reduce((a, v) => a + v.total, 0) / videos.length : 0;
|
|
69
|
+
const today = c.by_day.at(-1);
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
<p className="usage-number">
|
|
73
|
+
{usd(c.total)}
|
|
74
|
+
<small>
|
|
75
|
+
em {c.turns} turnos de {c.sessions} sessões
|
|
76
|
+
{videos.length > 0 && ` · média de ${usd(perVideo)} por vídeo`}
|
|
77
|
+
{today && ` · último dia (${dayLabel(today.day)}): ${usd(today.cost)}`}
|
|
78
|
+
</small>
|
|
79
|
+
</p>
|
|
80
|
+
{(c.partial > 0 || c.unmeasured > 0) && (
|
|
81
|
+
<ul className="usage-notices">
|
|
82
|
+
{c.partial > 0 && (
|
|
83
|
+
<li>
|
|
84
|
+
<span className="point warning" />
|
|
85
|
+
{c.partial} turno{c.partial === 1 ? "" : "s"} em andamento ou interrompido{c.partial === 1 ? "" : "s"}: o
|
|
86
|
+
valor é estimado até a próxima resposta fechar a conta.
|
|
87
|
+
</li>
|
|
88
|
+
)}
|
|
89
|
+
{c.unmeasured > 0 && (
|
|
90
|
+
<li>
|
|
91
|
+
<span className="point warning" />
|
|
92
|
+
{c.unmeasured} turno{c.unmeasured === 1 ? "" : "s"} do Codex fora da conta: ele só informa tokens, não
|
|
93
|
+
dólares.
|
|
94
|
+
</li>
|
|
95
|
+
)}
|
|
96
|
+
</ul>
|
|
97
|
+
)}
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function AgentCostPanel() {
|
|
103
|
+
const [days, setDays] = useState<number | null>(7);
|
|
104
|
+
const { cost, error } = useCost(days);
|
|
105
|
+
const videos = cost?.by_video.slice(0, 12) ?? [];
|
|
106
|
+
const topStage = (stages: Record<string, number>) => sorted(stages)[0];
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
<h2 className="usage-section">Agentes (Claude)</h2>
|
|
111
|
+
<Panel
|
|
112
|
+
title="O que a geração gastou"
|
|
113
|
+
extra={
|
|
114
|
+
<div className="segmented small" role="radiogroup" aria-label="Período">
|
|
115
|
+
{PERIODS.map(p => (
|
|
116
|
+
<button
|
|
117
|
+
key={p.label}
|
|
118
|
+
type="button"
|
|
119
|
+
role="radio"
|
|
120
|
+
aria-checked={days === p.days}
|
|
121
|
+
className={days === p.days ? "active" : ""}
|
|
122
|
+
onClick={() => setDays(p.days)}
|
|
123
|
+
>
|
|
124
|
+
{p.label}
|
|
125
|
+
</button>
|
|
126
|
+
))}
|
|
127
|
+
</div>
|
|
128
|
+
}
|
|
129
|
+
className="usage-card"
|
|
130
|
+
>
|
|
131
|
+
{error && <p className="response error">{error}</p>}
|
|
132
|
+
{!cost && !error && (
|
|
133
|
+
<div aria-busy="true">
|
|
134
|
+
<Skeleton className="row" style={{ width: "40%", height: 30 }} />
|
|
135
|
+
<Skeleton className="row" style={{ width: "100%", marginTop: 14 }} />
|
|
136
|
+
<Skeleton className="row" style={{ width: "85%", marginTop: 8 }} />
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
{cost && cost.turns === 0 && <p className="empty">Nenhum turno de agente neste período.</p>}
|
|
140
|
+
{cost && cost.turns > 0 && (
|
|
141
|
+
<>
|
|
142
|
+
<Summary c={cost} />
|
|
143
|
+
<p className="note">
|
|
144
|
+
Valor de tabela da API, o mesmo que o Claude Code reporta, também numa assinatura. Cada sessão retomada
|
|
145
|
+
acumula o total; aqui cada turno conta só o que gastou. A divisão por etapa e fork é proporcional às
|
|
146
|
+
mensagens, calibrada pelo total de cada modelo. No terminal: <code>node tools/cost.mjs</code>.
|
|
147
|
+
</p>
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
</Panel>
|
|
151
|
+
|
|
152
|
+
{cost && cost.turns > 0 && (
|
|
153
|
+
<>
|
|
154
|
+
<div className="usage-grid">
|
|
155
|
+
<Panel title="Por etapa" className="usage-card">
|
|
156
|
+
<Bars
|
|
157
|
+
entries={sorted(cost.by_stage).filter(e => e[1] >= 0.005)}
|
|
158
|
+
total={cost.total}
|
|
159
|
+
label={k => STAGE_LABEL[k] ?? k}
|
|
160
|
+
/>
|
|
161
|
+
</Panel>
|
|
162
|
+
<Panel title="Por modelo" className="usage-card">
|
|
163
|
+
<Bars entries={sorted(cost.by_model).filter(e => e[1] >= 0.005)} total={cost.total} label={k => k} />
|
|
164
|
+
<h3 className="cost-sub">Por tipo de sessão</h3>
|
|
165
|
+
<Bars entries={sorted(cost.by_kind)} total={cost.total} label={k => KIND_LABEL[k] ?? k} />
|
|
166
|
+
</Panel>
|
|
167
|
+
<Panel title="Por dia" className="usage-card">
|
|
168
|
+
<Bars
|
|
169
|
+
entries={cost.by_day
|
|
170
|
+
.slice(-14)
|
|
171
|
+
.reverse()
|
|
172
|
+
.map(d => [dayLabel(d.day), d.cost])}
|
|
173
|
+
total={cost.total}
|
|
174
|
+
label={k => k}
|
|
175
|
+
/>
|
|
176
|
+
</Panel>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<Panel title="Por vídeo">
|
|
180
|
+
<table className="table">
|
|
181
|
+
<thead>
|
|
182
|
+
<tr>
|
|
183
|
+
<th>Vídeo</th>
|
|
184
|
+
<th>Total</th>
|
|
185
|
+
<th>Turnos</th>
|
|
186
|
+
<th>Mais caro</th>
|
|
187
|
+
</tr>
|
|
188
|
+
</thead>
|
|
189
|
+
<tbody>
|
|
190
|
+
{videos.map(v => {
|
|
191
|
+
const top = topStage(v.stages);
|
|
192
|
+
return (
|
|
193
|
+
<tr key={v.video}>
|
|
194
|
+
<td>
|
|
195
|
+
{v.type === "video" || v.type === "edit" ? (
|
|
196
|
+
<Link to="/videos/$slug" params={{ slug: v.video }}>
|
|
197
|
+
{v.video}
|
|
198
|
+
</Link>
|
|
199
|
+
) : (
|
|
200
|
+
<>
|
|
201
|
+
{v.video} <span className="subtle">({KIND_LABEL[v.type] ?? v.type})</span>
|
|
202
|
+
</>
|
|
203
|
+
)}
|
|
204
|
+
</td>
|
|
205
|
+
<td>
|
|
206
|
+
<strong>{usd(v.total)}</strong>
|
|
207
|
+
</td>
|
|
208
|
+
<td>{v.turns}</td>
|
|
209
|
+
<td>
|
|
210
|
+
{top ? (
|
|
211
|
+
<>
|
|
212
|
+
{STAGE_LABEL[top[0]] ?? top[0]} <span className="subtle">{pct(top[1], v.total)}</span>
|
|
213
|
+
</>
|
|
214
|
+
) : (
|
|
215
|
+
<span className="subtle">—</span>
|
|
216
|
+
)}
|
|
217
|
+
</td>
|
|
218
|
+
</tr>
|
|
219
|
+
);
|
|
220
|
+
})}
|
|
221
|
+
</tbody>
|
|
222
|
+
</table>
|
|
223
|
+
</Panel>
|
|
224
|
+
|
|
225
|
+
<Panel title="Maiores forks">
|
|
226
|
+
<table className="table">
|
|
227
|
+
<thead>
|
|
228
|
+
<tr>
|
|
229
|
+
<th>Fork</th>
|
|
230
|
+
<th>Vídeo</th>
|
|
231
|
+
<th>Custo</th>
|
|
232
|
+
<th>Mensagens</th>
|
|
233
|
+
</tr>
|
|
234
|
+
</thead>
|
|
235
|
+
<tbody>
|
|
236
|
+
{cost.top_forks.slice(0, 8).map(f => (
|
|
237
|
+
<tr key={`${f.session}-${f.turn}-${f.label}`}>
|
|
238
|
+
<td>{f.label}</td>
|
|
239
|
+
<td className="subtle">{f.video}</td>
|
|
240
|
+
<td>
|
|
241
|
+
<strong>{usd(f.cost)}</strong>
|
|
242
|
+
</td>
|
|
243
|
+
<td>{f.messages}</td>
|
|
244
|
+
</tr>
|
|
245
|
+
))}
|
|
246
|
+
</tbody>
|
|
247
|
+
</table>
|
|
248
|
+
</Panel>
|
|
249
|
+
</>
|
|
250
|
+
)}
|
|
251
|
+
</>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/* Configurações → Agente de IA (hosted studio): the computers signed in to the account, with "Desconectar", and the
|
|
2
|
+
actions that went through to them. It replaces the Worker's old "Macs e ações" pages: the same information, inside
|
|
3
|
+
the app. Both come from the Worker itself, so they work while the studio isn't connected. */
|
|
4
|
+
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { useAudit, useComputers } from "../../../hooks/useComputers";
|
|
7
|
+
import { formatDayTime } from "../../../lib/format";
|
|
8
|
+
import { toast } from "../../../lib/toast";
|
|
9
|
+
import type { Computer } from "../../../types/api";
|
|
10
|
+
import { ArrowClockwise, Plug } from "../../../components/icons";
|
|
11
|
+
import { Badge } from "../../../components/ui/badge";
|
|
12
|
+
import { Panel } from "../../../components/ui/panel";
|
|
13
|
+
import { Skeleton } from "../../../components/ui/skeleton";
|
|
14
|
+
|
|
15
|
+
function ComputerRow({ c, onRevoke, busy }: { c: Computer; onRevoke: (id: string) => void; busy: boolean }) {
|
|
16
|
+
const [asking, setAsking] = useState(false);
|
|
17
|
+
return (
|
|
18
|
+
<li className="computer">
|
|
19
|
+
<span className="computer-icon" aria-hidden="true">
|
|
20
|
+
<Plug />
|
|
21
|
+
</span>
|
|
22
|
+
<span className="computer-text">
|
|
23
|
+
<b>{c.name}</b>
|
|
24
|
+
<span className="subtle">
|
|
25
|
+
{c.online
|
|
26
|
+
? "conectado agora"
|
|
27
|
+
: c.last_seen
|
|
28
|
+
? `visto por último ${formatDayTime(c.last_seen)}`
|
|
29
|
+
: "ainda não conectou"}{" "}
|
|
30
|
+
· entrou em {formatDayTime(c.created_at)}
|
|
31
|
+
{c.kind === "key" && " · acesso antigo"}
|
|
32
|
+
</span>
|
|
33
|
+
</span>
|
|
34
|
+
{c.online ? <Badge variant="ok">Conectado</Badge> : <Badge>Fora</Badge>}
|
|
35
|
+
{asking ? (
|
|
36
|
+
<span className="computer-ask">
|
|
37
|
+
<button type="button" className="button reject" disabled={busy} onClick={() => onRevoke(c.id)}>
|
|
38
|
+
Desconectar
|
|
39
|
+
</button>
|
|
40
|
+
<button type="button" className="button outlined" onClick={() => setAsking(false)}>
|
|
41
|
+
Cancelar
|
|
42
|
+
</button>
|
|
43
|
+
</span>
|
|
44
|
+
) : (
|
|
45
|
+
<button type="button" className="button outlined" onClick={() => setAsking(true)}>
|
|
46
|
+
Desconectar
|
|
47
|
+
</button>
|
|
48
|
+
)}
|
|
49
|
+
</li>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function Computers() {
|
|
54
|
+
const { email, computers, error, revoke } = useComputers(true);
|
|
55
|
+
const act = (id: string) =>
|
|
56
|
+
revoke.mutate(id, {
|
|
57
|
+
onSuccess: () => toast.success("Computador desconectado", { body: "Ele só volta entrando de novo." }),
|
|
58
|
+
onError: e => toast.error("Não consegui desconectar", { body: e.message }),
|
|
59
|
+
});
|
|
60
|
+
return (
|
|
61
|
+
<Panel title="Computadores conectados" extra={email && <span className="subtle">{email}</span>}>
|
|
62
|
+
<p className="subtle computers-intro">
|
|
63
|
+
Os computadores que entraram na sua conta pelo plugin do Claude Code. Desconectar encerra o acesso na hora; para
|
|
64
|
+
voltar, o computador entra de novo ao abrir o Claude Code.
|
|
65
|
+
</p>
|
|
66
|
+
{error && <p className="response error">{error}</p>}
|
|
67
|
+
{!computers && !error && (
|
|
68
|
+
<div aria-busy="true" aria-label="Carregando os computadores">
|
|
69
|
+
<Skeleton style={{ height: 52, width: "100%" }} />
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
{computers && computers.length === 0 && <p className="subtle">Nenhum computador entrou ainda.</p>}
|
|
73
|
+
{computers && computers.length > 0 && (
|
|
74
|
+
<ul className="computers">
|
|
75
|
+
{computers.map(c => (
|
|
76
|
+
<ComputerRow key={c.id} c={c} onRevoke={act} busy={revoke.isPending} />
|
|
77
|
+
))}
|
|
78
|
+
</ul>
|
|
79
|
+
)}
|
|
80
|
+
</Panel>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resultOf = (status: number) => (status === 503 ? "sem conexão" : status === 429 ? "limite" : String(status));
|
|
85
|
+
|
|
86
|
+
function Audit() {
|
|
87
|
+
const { entries, error, reload, loading } = useAudit(true);
|
|
88
|
+
return (
|
|
89
|
+
<Panel
|
|
90
|
+
title="Ações recentes"
|
|
91
|
+
extra={
|
|
92
|
+
<button type="button" className="button outlined" onClick={reload} disabled={loading}>
|
|
93
|
+
<ArrowClockwise className={`ic${loading ? " spin" : ""}`} aria-hidden="true" /> Atualizar
|
|
94
|
+
</button>
|
|
95
|
+
}
|
|
96
|
+
>
|
|
97
|
+
<p className="subtle computers-intro">
|
|
98
|
+
Tudo o que mudou algo e passou por aqui até o seu computador: quem, o quê, quando e como terminou. Só o caminho
|
|
99
|
+
fica registrado, nunca o conteúdo. Guarda as últimas 2000.
|
|
100
|
+
</p>
|
|
101
|
+
{error && <p className="response error">{error}</p>}
|
|
102
|
+
{!entries && !error && <Skeleton style={{ height: 120, width: "100%" }} />}
|
|
103
|
+
{entries && entries.length === 0 && <p className="subtle">Nenhuma ação ainda.</p>}
|
|
104
|
+
{entries && entries.length > 0 && (
|
|
105
|
+
<div className="audit-scroll">
|
|
106
|
+
<table className="table audit-table">
|
|
107
|
+
<thead>
|
|
108
|
+
<tr>
|
|
109
|
+
<th>Quando</th>
|
|
110
|
+
<th>Quem</th>
|
|
111
|
+
<th>Ação</th>
|
|
112
|
+
<th>Resultado</th>
|
|
113
|
+
</tr>
|
|
114
|
+
</thead>
|
|
115
|
+
<tbody>
|
|
116
|
+
{entries.map(e => (
|
|
117
|
+
<tr key={`${e.at}-${e.method}-${e.path}`}>
|
|
118
|
+
<td>{formatDayTime(e.at)}</td>
|
|
119
|
+
<td>{e.who}</td>
|
|
120
|
+
<td>
|
|
121
|
+
<code>
|
|
122
|
+
{e.method} {e.path}
|
|
123
|
+
</code>
|
|
124
|
+
</td>
|
|
125
|
+
<td className={e.status < 400 ? "audit-ok" : "audit-bad"}>
|
|
126
|
+
{resultOf(e.status)} · {e.ms} ms
|
|
127
|
+
</td>
|
|
128
|
+
</tr>
|
|
129
|
+
))}
|
|
130
|
+
</tbody>
|
|
131
|
+
</table>
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</Panel>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function ComputersPanel() {
|
|
139
|
+
return (
|
|
140
|
+
<>
|
|
141
|
+
<Computers />
|
|
142
|
+
<Audit />
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
}
|
package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx
CHANGED
|
@@ -40,8 +40,8 @@ export function OmnichatSettingsPanel({ omnichat: o, onSave }: { omnichat: Setti
|
|
|
40
40
|
>
|
|
41
41
|
<SettingRow label="Agente" help="Confere os comentários a cada minuto, com o studio aberto ou não">
|
|
42
42
|
<p className="hint">
|
|
43
|
-
Responde só nas regras ligadas. Não acorda
|
|
44
|
-
|
|
43
|
+
Responde só nas regras ligadas. Não acorda a máquina: dormindo, os comentários esperam e são respondidos
|
|
44
|
+
quando ela voltar, dentro dos 7 dias que a Meta dá para a DM.
|
|
45
45
|
</p>
|
|
46
46
|
{agent?.installed && !agent.node_ok && (
|
|
47
47
|
<p className="hint warning">O node que o agente usa não existe mais: desligue e ligue de novo.</p>
|
|
@@ -48,8 +48,8 @@ export function SchedulePanel({ publish: p, accounts, loading, onSave, reloadAcc
|
|
|
48
48
|
>
|
|
49
49
|
<SettingRow label="Agente" help="Confere a cada minuto, com o studio aberto ou não">
|
|
50
50
|
<p className="hint">
|
|
51
|
-
Publica na hora o que você agendou na etapa Publicar. Não acorda
|
|
52
|
-
voltar, até o atraso máximo abaixo.
|
|
51
|
+
Publica na hora o que você agendou na etapa Publicar. Não acorda a máquina: dormindo ou desligada, sobe quando
|
|
52
|
+
ela voltar, até o atraso máximo abaixo.
|
|
53
53
|
</p>
|
|
54
54
|
{agent?.installed && !agent.node_ok && (
|
|
55
55
|
<p className="hint warning">O node que o agente usa não existe mais: desligue e ligue de novo.</p>
|
|
@@ -66,7 +66,7 @@ export function SchedulePanel({ publish: p, accounts, loading, onSave, reloadAcc
|
|
|
66
66
|
</div>
|
|
67
67
|
<ResultMessage result={toggle.result} />
|
|
68
68
|
</SettingRow>
|
|
69
|
-
<SettingRow label="Atraso máximo" help="Com
|
|
69
|
+
<SettingRow label="Atraso máximo" help="Com a máquina fora do ar na hora, até quantas horas depois ainda publica">
|
|
70
70
|
<input
|
|
71
71
|
type="number"
|
|
72
72
|
min={0.25}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
/* Usage
|
|
2
|
-
Lives in Configurações → Uso
|
|
1
|
+
/* Usage and cost: what the agents spent (AgentCostPanel) and, below it, the paid voices: what is
|
|
2
|
+
left on Fish and Cartesia and what each video spent. Lives in Configurações → Uso e custo;
|
|
3
|
+
/usage opens there. */
|
|
3
4
|
|
|
4
5
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
5
6
|
import { Link } from "@tanstack/react-router";
|
|
@@ -13,6 +14,7 @@ import { ArrowClockwise, ArrowRight } from "../../../components/icons";
|
|
|
13
14
|
import { Badge } from "../../../components/ui/badge";
|
|
14
15
|
import { Panel } from "../../../components/ui/panel";
|
|
15
16
|
import { Skeleton } from "../../../components/ui/skeleton";
|
|
17
|
+
import { AgentCostPanel } from "./AgentCostPanel";
|
|
16
18
|
|
|
17
19
|
const num = (v: number | null | undefined, decimals = 0) =>
|
|
18
20
|
v == null ? "—" : v.toLocaleString("pt-BR", { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
|
@@ -257,6 +259,9 @@ export function UsageCategory() {
|
|
|
257
259
|
const d = usage && !("error" in usage) ? usage : null;
|
|
258
260
|
return (
|
|
259
261
|
<div className="usage-page">
|
|
262
|
+
<AgentCostPanel />
|
|
263
|
+
|
|
264
|
+
<h2 className="usage-section">Vozes</h2>
|
|
260
265
|
<header className="header">
|
|
261
266
|
{d && (
|
|
262
267
|
<span className="subtle">
|
|
@@ -12,6 +12,7 @@ import { VoiceCategory } from "../../components/VoiceCategory";
|
|
|
12
12
|
import { SettingsCategorySkeleton } from "../../components/SettingsCategorySkeleton";
|
|
13
13
|
import { VoiceProvidersCategory } from "./components/VoiceProvidersCategory";
|
|
14
14
|
import { UsageCategory } from "./components/UsageCategory";
|
|
15
|
+
import { useRunner } from "../../hooks/useRunner";
|
|
15
16
|
import { AgentCategory } from "./components/AgentCategory";
|
|
16
17
|
import { PlatformsCategory } from "./components/PlatformsCategory";
|
|
17
18
|
import { PublishingCategory } from "./components/PublishingCategory";
|
|
@@ -27,12 +28,16 @@ const CATEGORY_ICON: Partial<Record<SettingsCategoryId, ReactNode>> = {
|
|
|
27
28
|
|
|
28
29
|
export function SettingsPage({ category }: { category?: string }) {
|
|
29
30
|
const { settings, error, reload } = useSettings();
|
|
31
|
+
const { hosted } = useRunner();
|
|
30
32
|
const current = SETTINGS_CATEGORIES.find(c => c.id === category) ?? SETTINGS_CATEGORIES[0];
|
|
31
33
|
const inMenu = current.id === "providers" ? "voice" : current.id;
|
|
32
34
|
useEffect(() => {
|
|
33
35
|
document.title = `${current.name} · Configurações · Omni`;
|
|
34
36
|
}, [current.name]);
|
|
35
37
|
|
|
38
|
+
/* the computers of the hosted studio come from the Worker: the agent category shows them with no settings loaded */
|
|
39
|
+
const alone = current.id === "agent" && hosted && !settings;
|
|
40
|
+
|
|
36
41
|
const content: Record<SettingsCategoryId, (s: Settings) => ReactNode> = {
|
|
37
42
|
voice: s => <VoiceCategory settings={s} reload={reload} />,
|
|
38
43
|
providers: s => <VoiceProvidersCategory settings={s} reload={reload} />,
|
|
@@ -68,8 +73,17 @@ export function SettingsPage({ category }: { category?: string }) {
|
|
|
68
73
|
</nav>
|
|
69
74
|
</aside>
|
|
70
75
|
<div className="settings-content">
|
|
71
|
-
{error && <p className="response error">{error}</p>}
|
|
76
|
+
{error && !alone && <p className="response error">{error}</p>}
|
|
72
77
|
{!settings && !error && <SettingsCategorySkeleton />}
|
|
78
|
+
{alone && (
|
|
79
|
+
<section className="category" aria-labelledby="category-title">
|
|
80
|
+
<header className="category-header">
|
|
81
|
+
<h2 id="category-title">{current.name}</h2>
|
|
82
|
+
<p>O studio não está conectado agora: as contas e os modelos voltam quando ele conectar.</p>
|
|
83
|
+
</header>
|
|
84
|
+
<AgentCategory settings={null} reload={reload} />
|
|
85
|
+
</section>
|
|
86
|
+
)}
|
|
73
87
|
{settings && (
|
|
74
88
|
<section className="category" aria-labelledby="category-title">
|
|
75
89
|
<header className="category-header">
|
|
@@ -324,11 +324,14 @@ export function PublishForm({ video, current }: { video: VideoDetail; current: F
|
|
|
324
324
|
<CoverPicker video={video} platforms={chosen} />
|
|
325
325
|
{scheduling && (
|
|
326
326
|
<p className="hint">
|
|
327
|
-
{whenOk
|
|
327
|
+
{whenOk
|
|
328
|
+
? `Sobe ${formatDayTime(whenMs)}, hora desta máquina.`
|
|
329
|
+
: "Essa hora já passou: escolha outra na agenda."}{" "}
|
|
328
330
|
<button className="subtle-link" disabled={busy} onClick={pickInSchedule}>
|
|
329
331
|
Trocar dia e hora
|
|
330
332
|
</button>{" "}
|
|
331
|
-
O agente da agenda confere a cada minuto, com o studio aberto ou não;
|
|
333
|
+
O agente da agenda confere a cada minuto, com o studio aberto ou não; a máquina precisa estar ligada e
|
|
334
|
+
acordada.
|
|
332
335
|
{scheduled && " Substitui o agendamento de cima."}
|
|
333
336
|
</p>
|
|
334
337
|
)}
|
package/studio/web/router.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import { OmnichatPage, OmnichatRulePage } from "./pages/OmnichatPage";
|
|
|
22
22
|
import { GettingStartedPage } from "./pages/GettingStartedPage";
|
|
23
23
|
import { NotFoundPage } from "./pages/NotFoundPage";
|
|
24
24
|
import { BenchoPage } from "./pages/BenchoPage";
|
|
25
|
+
import { ConnectPreviewPage } from "./pages/ConnectPreviewPage";
|
|
25
26
|
|
|
26
27
|
export const rootRoute = createRootRoute({
|
|
27
28
|
component: AppShell,
|
|
@@ -175,6 +176,12 @@ export const benchoRoute = createRoute({
|
|
|
175
176
|
path: "/bencho",
|
|
176
177
|
component: () => <BenchoPage />,
|
|
177
178
|
});
|
|
179
|
+
/* the first-time and "sem conexão" screens with made-up data, to check them at 127.0.0.1; not in the menu */
|
|
180
|
+
export const connectPreviewRoute = createRoute({
|
|
181
|
+
getParentRoute: () => rootRoute,
|
|
182
|
+
path: "/preview/connect",
|
|
183
|
+
component: () => <ConnectPreviewPage />,
|
|
184
|
+
});
|
|
178
185
|
|
|
179
186
|
export const routeTree = rootRoute.addChildren([
|
|
180
187
|
indexRoute,
|
|
@@ -196,6 +203,7 @@ export const routeTree = rootRoute.addChildren([
|
|
|
196
203
|
omnichatRuleRoute,
|
|
197
204
|
gettingStartedRoute,
|
|
198
205
|
benchoRoute,
|
|
206
|
+
connectPreviewRoute,
|
|
199
207
|
]);
|
|
200
208
|
|
|
201
209
|
export const router = createRouter({ routeTree, defaultPreload: false, scrollRestoration: false });
|