@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
|
@@ -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
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* The sound picker: the kit grouped by role and the sounds captured for this video (music only for the bed). */
|
|
2
2
|
|
|
3
3
|
import { Picker } from "../../../../components/ui/picker";
|
|
4
|
-
import { roleLabel, round2 } from "../../../../lib/sound-timeline";
|
|
4
|
+
import { fileName, kitsOf, roleLabel, round2 } from "../../../../lib/sound-timeline";
|
|
5
5
|
import type { EffectInfo, SoundCatalog } from "./types";
|
|
6
6
|
|
|
7
7
|
export function EffectChoice({
|
|
@@ -18,7 +18,7 @@ export function EffectChoice({
|
|
|
18
18
|
const fits = (e: EffectInfo) => (bed ? true : !e.music);
|
|
19
19
|
const roles = Object.keys(catalog.roles);
|
|
20
20
|
const desc = (e: EffectInfo) =>
|
|
21
|
-
`${e.name} · ${e.music ? `${Math.round(e.duration)}s trilha` : round2(e.useful_end_s ?? e.duration)}`;
|
|
21
|
+
`${fileName(e.name)} · ${e.music ? `${Math.round(e.duration)}s trilha` : round2(e.useful_end_s ?? e.duration)}`;
|
|
22
22
|
return (
|
|
23
23
|
<Picker
|
|
24
24
|
aria-label="Som"
|
|
@@ -26,12 +26,14 @@ export function EffectChoice({
|
|
|
26
26
|
onChange={onChange}
|
|
27
27
|
placeholder="Escolha um som"
|
|
28
28
|
options={[
|
|
29
|
-
...
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
...kitsOf(catalog).flatMap(k =>
|
|
30
|
+
roles.flatMap(p => {
|
|
31
|
+
const ofRole = catalog.kit.filter(e => (e.kit ?? "") === k && e.role === p && fits(e));
|
|
32
|
+
return ofRole.length
|
|
33
|
+
? [{ group: `${k} · ${roleLabel(p)}`, options: ofRole.map(e => ({ value: e.name, label: desc(e) })) }]
|
|
34
|
+
: [];
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
35
37
|
...(catalog.video.filter(fits).length > 0
|
|
36
38
|
? [
|
|
37
39
|
{
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
/* The library:
|
|
1
|
+
/* The library: one tab per kit (the default kit first, open at start) and one for the video's own
|
|
2
|
+
sounds. Inside a tab, the sounds grouped by role. Drag onto the timeline or pick with a click
|
|
2
3
|
(then click the timeline); ▶ listens to the file. */
|
|
3
4
|
|
|
4
5
|
import { useState } from "react";
|
|
5
6
|
import { Play } from "../../../../components/icons";
|
|
6
7
|
import { previewFile } from "../../../../lib/sound-audio";
|
|
7
|
-
import { effectSrc,
|
|
8
|
+
import { effectSrc, fileName, kitLibrary, round2 } from "../../../../lib/sound-timeline";
|
|
8
9
|
import { AddSound } from "./AddSound";
|
|
9
|
-
import type {
|
|
10
|
+
import type { SoundCatalog } from "./types";
|
|
10
11
|
|
|
11
12
|
export function SoundLibrary({
|
|
12
13
|
catalog,
|
|
@@ -24,15 +25,8 @@ export function SoundLibrary({
|
|
|
24
25
|
onAdd: (name: string | null) => void;
|
|
25
26
|
}) {
|
|
26
27
|
const [filter, setFilter] = useState("");
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
!f || `${e.name} ${e.title} ${e.role ?? ""} ${e.music ? "música de fundo cama" : ""}`.toLowerCase().includes(f);
|
|
30
|
-
const groups: [string, EffectInfo[]][] = [
|
|
31
|
-
["deste vídeo", catalog.video.filter(passes)],
|
|
32
|
-
...Object.keys(catalog.roles).map(
|
|
33
|
-
p => [`kit · ${roleLabel(p)}`, catalog.kit.filter(e => e.role === p && passes(e))] as [string, EffectInfo[]],
|
|
34
|
-
),
|
|
35
|
-
];
|
|
28
|
+
const [picked, setPicked] = useState<string | null>(null);
|
|
29
|
+
const { tabs, tab, groups } = kitLibrary(catalog, picked, filter);
|
|
36
30
|
return (
|
|
37
31
|
<section className="sound-library">
|
|
38
32
|
<header>
|
|
@@ -46,14 +40,31 @@ export function SoundLibrary({
|
|
|
46
40
|
</header>
|
|
47
41
|
<p className="hint">Arraste para a timeline, ou clique num som e depois na timeline. ▶ ouve o arquivo.</p>
|
|
48
42
|
<AddSound slug={slug} onAdd={onAdd} />
|
|
43
|
+
<div className="segmented small sound-kit-tabs" role="tablist" aria-label="Kits de som">
|
|
44
|
+
{tabs.map(t => (
|
|
45
|
+
<button
|
|
46
|
+
key={t.id}
|
|
47
|
+
type="button"
|
|
48
|
+
role="tab"
|
|
49
|
+
aria-selected={tab === t.id}
|
|
50
|
+
className={tab === t.id ? "active" : ""}
|
|
51
|
+
onClick={() => setPicked(t.id)}
|
|
52
|
+
>
|
|
53
|
+
{t.name}
|
|
54
|
+
<span className="tab-count">{t.n}</span>
|
|
55
|
+
</button>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
49
58
|
<div className="sound-library-list">
|
|
50
59
|
{groups
|
|
51
60
|
.filter(([, l]) => l.length)
|
|
52
61
|
.map(([name, l]) => (
|
|
53
|
-
<div key={name} className="sound-group">
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
<div key={name || "video"} className="sound-group">
|
|
63
|
+
{name && (
|
|
64
|
+
<h5>
|
|
65
|
+
{name} <span className="subtle">{l.length}</span>
|
|
66
|
+
</h5>
|
|
67
|
+
)}
|
|
57
68
|
<div className="sound-cards">
|
|
58
69
|
{l.map(e => (
|
|
59
70
|
<div
|
|
@@ -86,13 +97,15 @@ export function SoundLibrary({
|
|
|
86
97
|
: round2(e.useful_end_s ?? e.duration)}
|
|
87
98
|
</span>
|
|
88
99
|
</span>
|
|
89
|
-
<span className="sound-item-name">{e.name.replace(/\.mp3$/, "")}</span>
|
|
100
|
+
<span className="sound-item-name">{fileName(e.name).replace(/\.mp3$/, "")}</span>
|
|
90
101
|
</div>
|
|
91
102
|
))}
|
|
92
103
|
</div>
|
|
93
104
|
</div>
|
|
94
105
|
))}
|
|
95
|
-
{groups.every(([, l]) => !l.length) &&
|
|
106
|
+
{groups.every(([, l]) => !l.length) && (
|
|
107
|
+
<p className="empty">Nenhum som{filter.trim() ? " com esse filtro" : ""}</p>
|
|
108
|
+
)}
|
|
96
109
|
</div>
|
|
97
110
|
</section>
|
|
98
111
|
);
|
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 });
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/* SetupSteps.tsx: the first-time setup as a vertical stepper. A circle on the left of each step (the line
|
|
2
|
+
between them runs down the circles' column), loading while it runs and an emerald check when it is done. */
|
|
3
|
+
.setup-steps {
|
|
4
|
+
list-style: none;
|
|
5
|
+
margin: 0;
|
|
6
|
+
padding: 0;
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
}
|
|
10
|
+
.setup-step {
|
|
11
|
+
position: relative;
|
|
12
|
+
display: grid;
|
|
13
|
+
grid-template-columns: 28px minmax(0, 1fr) auto;
|
|
14
|
+
align-items: start;
|
|
15
|
+
gap: 0 14px;
|
|
16
|
+
padding-bottom: 20px;
|
|
17
|
+
}
|
|
18
|
+
.setup-step:last-child {
|
|
19
|
+
padding-bottom: 0;
|
|
20
|
+
}
|
|
21
|
+
/* the line down to the next circle: emerald once this step is done */
|
|
22
|
+
.setup-step:not(:last-child)::before {
|
|
23
|
+
content: "";
|
|
24
|
+
position: absolute;
|
|
25
|
+
left: 13px;
|
|
26
|
+
top: 30px;
|
|
27
|
+
bottom: 2px;
|
|
28
|
+
width: 2px;
|
|
29
|
+
border-radius: 1px;
|
|
30
|
+
background: var(--border-strong);
|
|
31
|
+
transition: background 0.3s;
|
|
32
|
+
}
|
|
33
|
+
.setup-step.done:not(:last-child)::before {
|
|
34
|
+
background: color-mix(in srgb, var(--emerald) 70%, transparent);
|
|
35
|
+
}
|
|
36
|
+
.setup-mark {
|
|
37
|
+
position: relative;
|
|
38
|
+
z-index: 1;
|
|
39
|
+
display: grid;
|
|
40
|
+
place-items: center;
|
|
41
|
+
width: 28px;
|
|
42
|
+
height: 28px;
|
|
43
|
+
border-radius: 50%;
|
|
44
|
+
border: 2px solid var(--border-strong);
|
|
45
|
+
background: var(--bg);
|
|
46
|
+
color: var(--text-3);
|
|
47
|
+
font-size: 14px;
|
|
48
|
+
transition:
|
|
49
|
+
background 0.25s,
|
|
50
|
+
border-color 0.25s,
|
|
51
|
+
color 0.25s;
|
|
52
|
+
}
|
|
53
|
+
.setup-step.running .setup-mark {
|
|
54
|
+
border-color: var(--border-strong);
|
|
55
|
+
}
|
|
56
|
+
/* a white check on the dark emerald in both themes: the dark theme's --emerald is a light one, kept for text and lines */
|
|
57
|
+
.setup-step.done .setup-mark {
|
|
58
|
+
background: #059669;
|
|
59
|
+
border-color: #059669;
|
|
60
|
+
color: #fff;
|
|
61
|
+
animation: setup-pop 0.3s ease-out;
|
|
62
|
+
}
|
|
63
|
+
.setup-step.error .setup-mark {
|
|
64
|
+
background: var(--error-bg);
|
|
65
|
+
border-color: var(--error);
|
|
66
|
+
color: var(--error);
|
|
67
|
+
}
|
|
68
|
+
/* the loading ring inside the circle */
|
|
69
|
+
.setup-spin {
|
|
70
|
+
display: inline-block;
|
|
71
|
+
width: 14px;
|
|
72
|
+
height: 14px;
|
|
73
|
+
border-radius: 50%;
|
|
74
|
+
border: 2px solid var(--running);
|
|
75
|
+
border-top-color: transparent;
|
|
76
|
+
animation: spin 0.9s linear infinite;
|
|
77
|
+
}
|
|
78
|
+
.setup-text {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: 2px;
|
|
82
|
+
min-width: 0;
|
|
83
|
+
padding-top: 3px;
|
|
84
|
+
}
|
|
85
|
+
.setup-text b {
|
|
86
|
+
font-size: var(--el-text-ui);
|
|
87
|
+
font-weight: 500;
|
|
88
|
+
line-height: 20px;
|
|
89
|
+
color: var(--text);
|
|
90
|
+
}
|
|
91
|
+
.setup-step.pending .setup-text b {
|
|
92
|
+
color: var(--text-2);
|
|
93
|
+
}
|
|
94
|
+
.setup-detail {
|
|
95
|
+
font-size: var(--el-text-small);
|
|
96
|
+
line-height: 18px;
|
|
97
|
+
color: var(--text-3);
|
|
98
|
+
}
|
|
99
|
+
.setup-step.error .setup-detail {
|
|
100
|
+
color: var(--error);
|
|
101
|
+
}
|
|
102
|
+
.setup-error {
|
|
103
|
+
align-self: flex-start;
|
|
104
|
+
max-width: 100%;
|
|
105
|
+
margin-top: 4px;
|
|
106
|
+
padding: 2px 6px;
|
|
107
|
+
border-radius: 6px;
|
|
108
|
+
background: var(--muted);
|
|
109
|
+
font: 400 12px / 18px var(--mono);
|
|
110
|
+
color: var(--text-2);
|
|
111
|
+
overflow-wrap: anywhere;
|
|
112
|
+
}
|
|
113
|
+
.setup-said {
|
|
114
|
+
padding-top: 5px;
|
|
115
|
+
font-size: 12px;
|
|
116
|
+
line-height: 16px;
|
|
117
|
+
color: var(--text-3);
|
|
118
|
+
white-space: nowrap;
|
|
119
|
+
}
|
|
120
|
+
.setup-step.done .setup-said {
|
|
121
|
+
color: var(--emerald);
|
|
122
|
+
}
|
|
123
|
+
.setup-step.error .setup-said {
|
|
124
|
+
color: var(--error);
|
|
125
|
+
}
|
|
126
|
+
@keyframes setup-pop {
|
|
127
|
+
0% {
|
|
128
|
+
transform: scale(0.7);
|
|
129
|
+
}
|
|
130
|
+
60% {
|
|
131
|
+
transform: scale(1.12);
|
|
132
|
+
}
|
|
133
|
+
100% {
|
|
134
|
+
transform: scale(1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
@media (prefers-reduced-motion: reduce) {
|
|
138
|
+
.setup-step.done .setup-mark {
|
|
139
|
+
animation: none;
|
|
140
|
+
}
|
|
141
|
+
.setup-spin {
|
|
142
|
+
animation-duration: 2.4s;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
@media (max-width: 560px) {
|
|
146
|
+
.setup-said {
|
|
147
|
+
display: none;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* "Tentar de novo" on a failed step, in the place of its state */
|
|
152
|
+
.setup-step .setup-retry {
|
|
153
|
+
display: inline-flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: 6px;
|
|
156
|
+
height: 30px;
|
|
157
|
+
padding: 0 10px;
|
|
158
|
+
white-space: nowrap;
|
|
159
|
+
}
|
|
160
|
+
.setup-step .setup-retry .ic {
|
|
161
|
+
width: 14px;
|
|
162
|
+
height: 14px;
|
|
163
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* UserCard.tsx: under Omni's card in the menu, who is using the studio (avatar, name, email). Quieter than Omni's
|
|
2
|
+
card: the same shape, no border or light around it, and no click (hosted, only its sign-out button clicks). */
|
|
3
|
+
.menu .menu-user {
|
|
4
|
+
position: relative;
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: 8px;
|
|
8
|
+
width: 100%;
|
|
9
|
+
min-width: 0;
|
|
10
|
+
padding: 8px 8px 8px 10px;
|
|
11
|
+
border-radius: 16px;
|
|
12
|
+
background: var(--bg);
|
|
13
|
+
}
|
|
14
|
+
.menu .menu-user-initials,
|
|
15
|
+
.menu .menu-user-photo {
|
|
16
|
+
flex: none;
|
|
17
|
+
border-radius: 50%;
|
|
18
|
+
}
|
|
19
|
+
.menu .menu-user-photo {
|
|
20
|
+
display: block;
|
|
21
|
+
width: 36px;
|
|
22
|
+
height: 36px;
|
|
23
|
+
object-fit: cover;
|
|
24
|
+
}
|
|
25
|
+
/* no picture: two initials on a soft disc */
|
|
26
|
+
.menu .menu-user-initials {
|
|
27
|
+
display: grid;
|
|
28
|
+
place-items: center;
|
|
29
|
+
width: 36px;
|
|
30
|
+
height: 36px;
|
|
31
|
+
background: var(--surface-3);
|
|
32
|
+
font-size: 13px;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
letter-spacing: 0.02em;
|
|
35
|
+
color: var(--text-2);
|
|
36
|
+
}
|
|
37
|
+
.menu .menu-user-text {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex: 1;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
min-width: 0;
|
|
42
|
+
line-height: 18px;
|
|
43
|
+
}
|
|
44
|
+
.menu .menu-user-text b {
|
|
45
|
+
font-size: 14px;
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
color: var(--text);
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
text-overflow: ellipsis;
|
|
50
|
+
white-space: nowrap;
|
|
51
|
+
}
|
|
52
|
+
.menu .menu-user-text span {
|
|
53
|
+
font-size: 12px;
|
|
54
|
+
color: var(--text-3);
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
text-overflow: ellipsis;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
}
|
|
59
|
+
/* collapsed menu: just the avatar */
|
|
60
|
+
.menu-collapsed .menu .menu-user {
|
|
61
|
+
justify-content: center;
|
|
62
|
+
padding: 6px 0;
|
|
63
|
+
background: none;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Sair (hosted): an icon button on the right of the card, the look of the menu's collapse button */
|
|
67
|
+
.menu .menu-user-exit {
|
|
68
|
+
display: contents;
|
|
69
|
+
}
|
|
70
|
+
.menu .menu-user-signout {
|
|
71
|
+
display: inline-grid;
|
|
72
|
+
place-items: center;
|
|
73
|
+
width: 32px;
|
|
74
|
+
height: 32px;
|
|
75
|
+
flex: none;
|
|
76
|
+
margin-left: auto;
|
|
77
|
+
padding: 0;
|
|
78
|
+
border: 0;
|
|
79
|
+
border-radius: var(--el-radius-nav);
|
|
80
|
+
background: transparent;
|
|
81
|
+
color: var(--text-3);
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
}
|
|
84
|
+
.menu .menu-user-signout svg {
|
|
85
|
+
width: 20px;
|
|
86
|
+
height: 20px;
|
|
87
|
+
}
|
|
88
|
+
.menu .menu-user-signout:hover,
|
|
89
|
+
.menu .menu-user-signout:focus-visible {
|
|
90
|
+
background: var(--el-muted);
|
|
91
|
+
color: var(--text);
|
|
92
|
+
}
|
|
93
|
+
/* collapsed (the menu only collapses on a wide screen), the button sits over the avatar and appears, with a
|
|
94
|
+
fade, when the pointer is on it; the avatar fades out under it, as the logo does for the collapse button */
|
|
95
|
+
@media (min-width: 901px) {
|
|
96
|
+
.menu-collapsed .menu .menu-user-signout {
|
|
97
|
+
position: absolute;
|
|
98
|
+
top: 50%;
|
|
99
|
+
left: 50%;
|
|
100
|
+
margin: -16px 0 0 -16px;
|
|
101
|
+
opacity: 0;
|
|
102
|
+
transition:
|
|
103
|
+
opacity 0.18s ease,
|
|
104
|
+
background 0.12s ease,
|
|
105
|
+
color 0.12s ease;
|
|
106
|
+
}
|
|
107
|
+
.menu-collapsed .menu .menu-user:hover .menu-user-signout,
|
|
108
|
+
.menu-collapsed .menu .menu-user-signout:focus-visible {
|
|
109
|
+
opacity: 1;
|
|
110
|
+
}
|
|
111
|
+
.menu-collapsed .menu .menu-user-photo,
|
|
112
|
+
.menu-collapsed .menu .menu-user-initials {
|
|
113
|
+
transition: opacity 0.18s ease;
|
|
114
|
+
}
|
|
115
|
+
.menu-collapsed .menu .menu-user:hover .menu-user-photo,
|
|
116
|
+
.menu-collapsed .menu .menu-user:hover .menu-user-initials,
|
|
117
|
+
.menu-collapsed .menu .menu-user:has(.menu-user-signout:focus-visible) .menu-user-photo,
|
|
118
|
+
.menu-collapsed .menu .menu-user:has(.menu-user-signout:focus-visible) .menu-user-initials {
|
|
119
|
+
opacity: 0;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
@import "./pages/video-list/list.css";
|
|
25
25
|
@import "./components/badge.css";
|
|
26
26
|
@import "./components/stepper.css";
|
|
27
|
+
@import "./components/setup-steps.css";
|
|
28
|
+
@import "./components/user-card.css";
|
|
27
29
|
@import "./components/panel.css";
|
|
28
30
|
@import "./components/queue.css";
|
|
29
31
|
@import "./pages/video/layout.css";
|
|
@@ -363,3 +363,82 @@
|
|
|
363
363
|
.agent-missing small {
|
|
364
364
|
flex-basis: 100%;
|
|
365
365
|
}
|
|
366
|
+
|
|
367
|
+
/* Configurações → Agente de IA (hosted): the computers signed in and the actions that went through */
|
|
368
|
+
.computers-intro {
|
|
369
|
+
margin: 0 0 12px;
|
|
370
|
+
max-width: 70ch;
|
|
371
|
+
font-size: var(--el-text-small);
|
|
372
|
+
line-height: 18px;
|
|
373
|
+
}
|
|
374
|
+
.computers {
|
|
375
|
+
list-style: none;
|
|
376
|
+
margin: 0;
|
|
377
|
+
padding: 0;
|
|
378
|
+
display: flex;
|
|
379
|
+
flex-direction: column;
|
|
380
|
+
}
|
|
381
|
+
.computer {
|
|
382
|
+
display: grid;
|
|
383
|
+
grid-template-columns: 36px minmax(0, 1fr) auto auto;
|
|
384
|
+
align-items: center;
|
|
385
|
+
gap: 12px;
|
|
386
|
+
padding: 12px 0;
|
|
387
|
+
border-top: 1px solid var(--border);
|
|
388
|
+
}
|
|
389
|
+
.computer:first-child {
|
|
390
|
+
border-top: 0;
|
|
391
|
+
padding-top: 0;
|
|
392
|
+
}
|
|
393
|
+
.computer-icon {
|
|
394
|
+
display: grid;
|
|
395
|
+
place-items: center;
|
|
396
|
+
width: 36px;
|
|
397
|
+
height: 36px;
|
|
398
|
+
border-radius: var(--el-radius-control);
|
|
399
|
+
background: var(--muted);
|
|
400
|
+
color: var(--text-2);
|
|
401
|
+
font-size: 18px;
|
|
402
|
+
}
|
|
403
|
+
.computer-text {
|
|
404
|
+
display: flex;
|
|
405
|
+
flex-direction: column;
|
|
406
|
+
min-width: 0;
|
|
407
|
+
gap: 1px;
|
|
408
|
+
}
|
|
409
|
+
.computer-text b {
|
|
410
|
+
font-weight: 500;
|
|
411
|
+
font-size: var(--el-text-ui);
|
|
412
|
+
}
|
|
413
|
+
.computer-text .subtle {
|
|
414
|
+
font-size: var(--el-text-small);
|
|
415
|
+
}
|
|
416
|
+
.computer-ask {
|
|
417
|
+
display: inline-flex;
|
|
418
|
+
gap: 6px;
|
|
419
|
+
}
|
|
420
|
+
.audit-scroll {
|
|
421
|
+
max-height: 420px;
|
|
422
|
+
overflow: auto;
|
|
423
|
+
}
|
|
424
|
+
.audit-table code {
|
|
425
|
+
font: 400 12px / 18px var(--mono);
|
|
426
|
+
overflow-wrap: anywhere;
|
|
427
|
+
}
|
|
428
|
+
.audit-table .audit-ok {
|
|
429
|
+
color: var(--ok);
|
|
430
|
+
}
|
|
431
|
+
.audit-table .audit-bad {
|
|
432
|
+
color: var(--error);
|
|
433
|
+
}
|
|
434
|
+
@media (max-width: 720px) {
|
|
435
|
+
.computer {
|
|
436
|
+
grid-template-columns: 36px minmax(0, 1fr);
|
|
437
|
+
}
|
|
438
|
+
.computer > .badge,
|
|
439
|
+
.computer > .button,
|
|
440
|
+
.computer > .computer-ask {
|
|
441
|
+
grid-column: 2;
|
|
442
|
+
justify-self: start;
|
|
443
|
+
}
|
|
444
|
+
}
|