@crievo/omni 0.1.7 → 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.
Files changed (68) hide show
  1. package/omnivideo.config.example.json +1 -0
  2. package/package.json +1 -1
  3. package/reference/cloud.md +15 -1
  4. package/reference/learnings.md +7 -0
  5. package/reference/studio.md +1 -0
  6. package/studio/api/app.ts +4 -0
  7. package/studio/api/routes/cost.ts +13 -0
  8. package/studio/api/routes/me.ts +8 -0
  9. package/studio/api/services/agent-tools.ts +2 -2
  10. package/studio/api/services/cost.ts +22 -0
  11. package/studio/api/services/me.ts +67 -0
  12. package/studio/api/services/sessions.ts +8 -2
  13. package/studio/runner/runner.ts +57 -10
  14. package/studio/shared/omni-screens.ts +1 -1
  15. package/studio/shared/schemas/cost.ts +10 -0
  16. package/studio/shared/types/api.ts +74 -0
  17. package/studio/studio.ts +27 -2
  18. package/studio/web/components/AppShell.tsx +11 -3
  19. package/studio/web/components/FirstConnection.tsx +222 -0
  20. package/studio/web/components/OfflineNotice.tsx +59 -0
  21. package/studio/web/components/SetupSteps.tsx +63 -0
  22. package/studio/web/components/Sidebar.tsx +10 -25
  23. package/studio/web/components/StudioOffline.tsx +48 -50
  24. package/studio/web/components/UserCard.tsx +79 -0
  25. package/studio/web/components/icons.tsx +2 -0
  26. package/studio/web/hooks/useComputers.ts +55 -0
  27. package/studio/web/hooks/useCost.ts +11 -0
  28. package/studio/web/hooks/useMe.ts +32 -0
  29. package/studio/web/hooks/useSetupProgress.ts +54 -0
  30. package/studio/web/lib/me.ts +17 -0
  31. package/studio/web/lib/query-client.ts +6 -0
  32. package/studio/web/lib/settings-categories.ts +2 -2
  33. package/studio/web/lib/setup-retry.ts +29 -0
  34. package/studio/web/lib/studio-offline.ts +105 -59
  35. package/studio/web/pages/ConnectPreviewPage/index.tsx +215 -0
  36. package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +18 -9
  37. package/studio/web/pages/SettingsPage/components/AgentCostPanel.tsx +253 -0
  38. package/studio/web/pages/SettingsPage/components/ComputersPanel.tsx +145 -0
  39. package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +2 -2
  40. package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +3 -3
  41. package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +7 -2
  42. package/studio/web/pages/SettingsPage/index.tsx +15 -1
  43. package/studio/web/pages/VideoPage/components/PublishForm.tsx +5 -2
  44. package/studio/web/router.tsx +8 -0
  45. package/studio/web/styles/components/setup-steps.css +163 -0
  46. package/studio/web/styles/components/user-card.css +121 -0
  47. package/studio/web/styles/index.css +2 -0
  48. package/studio/web/styles/pages/settings/shell.css +79 -0
  49. package/studio/web/styles/pages/settings/usage.css +47 -0
  50. package/studio/web/styles/pages/studio-offline.css +224 -38
  51. package/tools/cost.mjs +114 -0
  52. package/tools/lib/agents.mjs +3 -2
  53. package/tools/lib/channel.mjs +1 -1
  54. package/tools/lib/cloud-user.mjs +58 -0
  55. package/tools/lib/cost.mjs +357 -0
  56. package/tools/lib/deps.mjs +27 -12
  57. package/tools/lib/oauth-client.mjs +28 -14
  58. package/tools/lib/pages-style.mjs +67 -0
  59. package/tools/lib/pairing.mjs +1 -1
  60. package/tools/lib/schedule/schedule.mjs +1 -1
  61. package/tools/lib/self-update.mjs +122 -0
  62. package/tools/lib/setup-progress.mjs +315 -0
  63. package/tools/lib/up.mjs +176 -61
  64. package/tools/metrics.mjs +6 -1
  65. package/tools/omni.mjs +41 -16
  66. package/tools/omnichat.mjs +45 -9
  67. package/tools/platforms.mjs +75 -14
  68. 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
- <Panel title="Contas de agente">
12
- <AgentAccounts agents={settings.agents} onSave={reload} />
13
- <CodexSettings codex={settings.agents.codex} onSave={reload} />
14
- </Panel>
15
- <Panel title="Modelos do Claude">
16
- <ModelsSettings models={settings.models} onSave={reload} />
17
- </Panel>
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
+ }
@@ -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 o Mac: dormindo, os comentários esperam e são respondidos quando
44
- ele voltar, dentro dos 7 dias que a Meta dá para a DM.
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 o Mac: dormindo ou desligado, sobe quando ele
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 o Mac fora do ar na hora, até quantas horas depois ainda publica">
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 of the paid voices: what is left on Fish and Cartesia and what each video spent.
2
- Lives in Configurações → Uso das vozes; /usage opens there. */
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 ? `Sobe ${formatDayTime(whenMs)}, hora deste Mac.` : "Essa hora já passou: escolha outra na agenda."}{" "}
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; o Mac precisa estar ligado e acordado.
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
  )}
@@ -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 });