@crievo/omni 0.1.7 → 0.1.9
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 +2 -0
- package/package.json +1 -1
- package/reference/cloud.md +15 -1
- package/reference/learnings.md +7 -0
- package/reference/scenes-contract.md +3 -2
- package/reference/studio.md +3 -2
- 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-persona.ts +1 -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/api/services/sound.ts +17 -6
- 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 +77 -1
- 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/sound-timeline.ts +33 -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/pages/VideoPage/components/SoundTimeline/EffectChoice.tsx +10 -8
- package/studio/web/pages/VideoPage/components/SoundTimeline/SoundLibrary.tsx +31 -18
- 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 +3 -2
- 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
- package/tools/setup.mjs +18 -5
- package/tools/sound.mjs +198 -94
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/* /preview/connect: the first-time screen and the "sem conexão" ones, with the real components and made-up data, to
|
|
2
|
+
check them without a hosted studio (at 127.0.0.1 the studio is always connected, so none of them shows up by
|
|
3
|
+
itself). Not in the menu: reached by the address. "Reproduzir" walks the setup the way the CLI reports it, step by
|
|
4
|
+
step; the other tabs are fixed moments of it. In the hosted studio the same components read the real report
|
|
5
|
+
(hooks/useSetupProgress.ts) and the hub's state (hooks/useRunner.ts). */
|
|
6
|
+
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
import { FirstConnection } from "../../components/FirstConnection";
|
|
9
|
+
import { OfflineNotice } from "../../components/OfflineNotice";
|
|
10
|
+
import { offlineAdvice } from "../../lib/studio-offline";
|
|
11
|
+
import type { RunnerProblem, SetupRecord, SetupStepState } from "../../types/api";
|
|
12
|
+
|
|
13
|
+
const STEPS = [
|
|
14
|
+
[
|
|
15
|
+
"workspace",
|
|
16
|
+
"Criando a sua pasta de trabalho",
|
|
17
|
+
"Onde ficam os seus vídeos e as suas configurações, no seu computador.",
|
|
18
|
+
],
|
|
19
|
+
["app", "Instalando o Omni neste computador", "Uma cópia que se atualiza sozinha e fica pronta para o Claude usar."],
|
|
20
|
+
["video", "Configuração para montar e exportar o vídeo", "Junta as cenas, a voz e as legendas no arquivo final."],
|
|
21
|
+
["render", "Configuração para renderizar as animações", "Transforma o motion de cada cena em vídeo."],
|
|
22
|
+
[
|
|
23
|
+
"voice",
|
|
24
|
+
"Configuração para a voz, o tempo e as legendas",
|
|
25
|
+
"Mede a fala e deixa cada cena e cada legenda no tempo certo.",
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"style",
|
|
29
|
+
"Configuração para extrair o estilo visual",
|
|
30
|
+
"Lê um vídeo ou um link de referência e aprende o estilo dele.",
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
"studio",
|
|
34
|
+
"Conectando este computador ao studio",
|
|
35
|
+
"Para o site conversar com o Claude e com os seus arquivos daqui.",
|
|
36
|
+
],
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
/* a record with the first `done` steps finished and the next one in the given state */
|
|
40
|
+
function record(done: number, next: SetupStepState = "running", error?: string): SetupRecord {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
return {
|
|
43
|
+
v: 1,
|
|
44
|
+
state: done >= STEPS.length ? "done" : next === "error" ? "error" : "running",
|
|
45
|
+
device: "MacBook-Pro",
|
|
46
|
+
started_at: now - 120_000,
|
|
47
|
+
updated_at: now,
|
|
48
|
+
steps: STEPS.map(([id, label, detail], i) => ({
|
|
49
|
+
id,
|
|
50
|
+
label,
|
|
51
|
+
detail,
|
|
52
|
+
state: i < done ? "done" : i === done ? next : "pending",
|
|
53
|
+
...(i === done && next === "error" && error ? { error } : {}),
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* the trouble began a moment ago: the normal state. `long` is the same an hour on, when the last-resort line shows */
|
|
59
|
+
const at = new Date(Date.now() - 20_000).toISOString();
|
|
60
|
+
const long = new Date(Date.now() - 3_600_000).toISOString();
|
|
61
|
+
const problem = (p: RunnerProblem) => p;
|
|
62
|
+
|
|
63
|
+
/* the failed step with the computer listening: "Tentar de novo" asks, the step loads, and it works the second time */
|
|
64
|
+
function ErrorDemo() {
|
|
65
|
+
const [phase, setPhase] = useState<"error" | "asking" | "fixed">("error");
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (phase !== "asking") return;
|
|
68
|
+
const id = setTimeout(() => setPhase("fixed"), 2200);
|
|
69
|
+
return () => clearTimeout(id);
|
|
70
|
+
}, [phase]);
|
|
71
|
+
const setup =
|
|
72
|
+
phase === "fixed"
|
|
73
|
+
? record(STEPS.length)
|
|
74
|
+
: { ...record(4, "error", "não consegui baixar o instalador (sem internet?)"), watching_at: Date.now() };
|
|
75
|
+
return (
|
|
76
|
+
<FirstConnection
|
|
77
|
+
setup={setup}
|
|
78
|
+
retry={{ listening: true, pending: phase === "asking" ? "voice" : null, onRetry: () => setPhase("asking") }}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type View = { id: string; name: string; render: () => React.ReactNode };
|
|
84
|
+
|
|
85
|
+
const VIEWS: View[] = [
|
|
86
|
+
{ id: "install", name: "1 · Instalar o plugin", render: () => <FirstConnection setup={null} /> },
|
|
87
|
+
{ id: "running", name: "2 · Preparando", render: () => <FirstConnection setup={record(3)} /> },
|
|
88
|
+
{ id: "error", name: "3 · Etapa com erro", render: () => <ErrorDemo /> },
|
|
89
|
+
{
|
|
90
|
+
id: "stale",
|
|
91
|
+
name: "3 · Erro, sem resposta",
|
|
92
|
+
render: () => (
|
|
93
|
+
<FirstConnection
|
|
94
|
+
setup={record(4, "error", "não consegui baixar o instalador (sem internet?)")}
|
|
95
|
+
retry={{ listening: false, pending: null, onRetry: () => {} }}
|
|
96
|
+
/>
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
{ id: "waiting", name: "4 · Conectando o studio", render: () => <FirstConnection setup={record(STEPS.length)} /> },
|
|
100
|
+
{
|
|
101
|
+
id: "lost",
|
|
102
|
+
name: "Perdeu a conexão",
|
|
103
|
+
render: () => (
|
|
104
|
+
<OfflineNotice
|
|
105
|
+
advice={offlineAdvice({ name: "MacBook-Pro", lastSeen: at, problem: null })}
|
|
106
|
+
checking={false}
|
|
107
|
+
onCheck={() => {}}
|
|
108
|
+
/>
|
|
109
|
+
),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "lost-long",
|
|
113
|
+
name: "Perdeu há 1 h",
|
|
114
|
+
render: () => (
|
|
115
|
+
<OfflineNotice
|
|
116
|
+
advice={offlineAdvice({ name: "MacBook-Pro", lastSeen: long, problem: null })}
|
|
117
|
+
checking={false}
|
|
118
|
+
onCheck={() => {}}
|
|
119
|
+
/>
|
|
120
|
+
),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "key",
|
|
124
|
+
name: "Precisa aprovar",
|
|
125
|
+
render: () => (
|
|
126
|
+
<OfflineNotice
|
|
127
|
+
advice={offlineAdvice({
|
|
128
|
+
name: null,
|
|
129
|
+
lastSeen: null,
|
|
130
|
+
problem: problem({ reason: "key", at, host: "MacBook-Pro" }),
|
|
131
|
+
})}
|
|
132
|
+
checking={false}
|
|
133
|
+
onCheck={() => {}}
|
|
134
|
+
/>
|
|
135
|
+
),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "revoked",
|
|
139
|
+
name: "Desconectado",
|
|
140
|
+
render: () => (
|
|
141
|
+
<OfflineNotice
|
|
142
|
+
advice={offlineAdvice({ name: "MacBook-Pro", lastSeen: at, problem: problem({ reason: "revoked", at }) })}
|
|
143
|
+
checking={false}
|
|
144
|
+
onCheck={() => {}}
|
|
145
|
+
/>
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
/* "Reproduzir": one step finishes every couple of seconds, the way the CLI reports it, and it ends on the studio connecting */
|
|
151
|
+
const STEP_MS = 2000;
|
|
152
|
+
|
|
153
|
+
export function ConnectPreviewPage() {
|
|
154
|
+
/* ?v=<id> opens straight on a state (the ids of VIEWS) */
|
|
155
|
+
const [view, setView] = useState(() => {
|
|
156
|
+
const v = new URLSearchParams(location.search).get("v");
|
|
157
|
+
return VIEWS.some(x => x.id === v) ? (v as string) : "install";
|
|
158
|
+
});
|
|
159
|
+
const [playing, setPlaying] = useState(false);
|
|
160
|
+
/* how many steps are done while playing */
|
|
161
|
+
const [done, setDone] = useState(0);
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
document.title = "Pré-visualização do onboarding · Omni";
|
|
165
|
+
}, []);
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!playing) return;
|
|
168
|
+
const id = setInterval(() => setDone(d => d + 1), STEP_MS);
|
|
169
|
+
return () => clearInterval(id);
|
|
170
|
+
}, [playing]);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (playing && done >= STEPS.length) {
|
|
173
|
+
setPlaying(false);
|
|
174
|
+
setView("waiting");
|
|
175
|
+
}
|
|
176
|
+
}, [playing, done]);
|
|
177
|
+
|
|
178
|
+
const play = () => {
|
|
179
|
+
setDone(0);
|
|
180
|
+
setPlaying(true);
|
|
181
|
+
};
|
|
182
|
+
const current = VIEWS.find(v => v.id === view) ?? VIEWS[0];
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div className="page">
|
|
186
|
+
<header className="header">
|
|
187
|
+
<h1>
|
|
188
|
+
<em>Pré-visualização</em> do onboarding
|
|
189
|
+
</h1>
|
|
190
|
+
<span className="subtle">dados de exemplo: no studio hospedado vêm do computador de verdade</span>
|
|
191
|
+
</header>
|
|
192
|
+
<div className="actions" style={{ alignItems: "center", marginBottom: 16 }}>
|
|
193
|
+
<div className="segmented" role="group" aria-label="Estado">
|
|
194
|
+
{VIEWS.map(v => (
|
|
195
|
+
<button
|
|
196
|
+
key={v.id}
|
|
197
|
+
type="button"
|
|
198
|
+
className={!playing && view === v.id ? "active" : ""}
|
|
199
|
+
onClick={() => {
|
|
200
|
+
setPlaying(false);
|
|
201
|
+
setView(v.id);
|
|
202
|
+
}}
|
|
203
|
+
>
|
|
204
|
+
{v.name}
|
|
205
|
+
</button>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
<button type="button" className="button primary" onClick={play} disabled={playing}>
|
|
209
|
+
{playing ? "Reproduzindo…" : "Reproduzir o setup"}
|
|
210
|
+
</button>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="connect-preview">{playing ? <FirstConnection setup={record(done)} /> : current.render()}</div>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
@@ -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>
|