@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.
Files changed (76) hide show
  1. package/omnivideo.config.example.json +2 -0
  2. package/package.json +1 -1
  3. package/reference/cloud.md +15 -1
  4. package/reference/learnings.md +7 -0
  5. package/reference/scenes-contract.md +3 -2
  6. package/reference/studio.md +3 -2
  7. package/studio/api/app.ts +4 -0
  8. package/studio/api/routes/cost.ts +13 -0
  9. package/studio/api/routes/me.ts +8 -0
  10. package/studio/api/services/agent-persona.ts +1 -0
  11. package/studio/api/services/agent-tools.ts +2 -2
  12. package/studio/api/services/cost.ts +22 -0
  13. package/studio/api/services/me.ts +67 -0
  14. package/studio/api/services/sessions.ts +8 -2
  15. package/studio/api/services/sound.ts +17 -6
  16. package/studio/runner/runner.ts +57 -10
  17. package/studio/shared/omni-screens.ts +1 -1
  18. package/studio/shared/schemas/cost.ts +10 -0
  19. package/studio/shared/types/api.ts +77 -1
  20. package/studio/studio.ts +27 -2
  21. package/studio/web/components/AppShell.tsx +11 -3
  22. package/studio/web/components/FirstConnection.tsx +222 -0
  23. package/studio/web/components/OfflineNotice.tsx +59 -0
  24. package/studio/web/components/SetupSteps.tsx +63 -0
  25. package/studio/web/components/Sidebar.tsx +10 -25
  26. package/studio/web/components/StudioOffline.tsx +48 -50
  27. package/studio/web/components/UserCard.tsx +79 -0
  28. package/studio/web/components/icons.tsx +2 -0
  29. package/studio/web/hooks/useComputers.ts +55 -0
  30. package/studio/web/hooks/useCost.ts +11 -0
  31. package/studio/web/hooks/useMe.ts +32 -0
  32. package/studio/web/hooks/useSetupProgress.ts +54 -0
  33. package/studio/web/lib/me.ts +17 -0
  34. package/studio/web/lib/query-client.ts +6 -0
  35. package/studio/web/lib/settings-categories.ts +2 -2
  36. package/studio/web/lib/setup-retry.ts +29 -0
  37. package/studio/web/lib/sound-timeline.ts +33 -0
  38. package/studio/web/lib/studio-offline.ts +105 -59
  39. package/studio/web/pages/ConnectPreviewPage/index.tsx +215 -0
  40. package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +18 -9
  41. package/studio/web/pages/SettingsPage/components/AgentCostPanel.tsx +253 -0
  42. package/studio/web/pages/SettingsPage/components/ComputersPanel.tsx +145 -0
  43. package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +2 -2
  44. package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +3 -3
  45. package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +7 -2
  46. package/studio/web/pages/SettingsPage/index.tsx +15 -1
  47. package/studio/web/pages/VideoPage/components/PublishForm.tsx +5 -2
  48. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectChoice.tsx +10 -8
  49. package/studio/web/pages/VideoPage/components/SoundTimeline/SoundLibrary.tsx +31 -18
  50. package/studio/web/router.tsx +8 -0
  51. package/studio/web/styles/components/setup-steps.css +163 -0
  52. package/studio/web/styles/components/user-card.css +121 -0
  53. package/studio/web/styles/index.css +2 -0
  54. package/studio/web/styles/pages/settings/shell.css +79 -0
  55. package/studio/web/styles/pages/settings/usage.css +47 -0
  56. package/studio/web/styles/pages/studio-offline.css +224 -38
  57. package/tools/cost.mjs +114 -0
  58. package/tools/lib/agents.mjs +3 -2
  59. package/tools/lib/channel.mjs +1 -1
  60. package/tools/lib/cloud-user.mjs +58 -0
  61. package/tools/lib/cost.mjs +357 -0
  62. package/tools/lib/deps.mjs +27 -12
  63. package/tools/lib/oauth-client.mjs +28 -14
  64. package/tools/lib/pages-style.mjs +67 -0
  65. package/tools/lib/pairing.mjs +1 -1
  66. package/tools/lib/schedule/schedule.mjs +1 -1
  67. package/tools/lib/self-update.mjs +122 -0
  68. package/tools/lib/setup-progress.mjs +315 -0
  69. package/tools/lib/up.mjs +176 -61
  70. package/tools/metrics.mjs +6 -1
  71. package/tools/omni.mjs +41 -16
  72. package/tools/omnichat.mjs +45 -9
  73. package/tools/platforms.mjs +75 -14
  74. package/tools/runner.mjs +6 -6
  75. package/tools/setup.mjs +18 -5
  76. 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 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
  )}
@@ -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
- ...roles.flatMap(p => {
30
- const ofRole = catalog.kit.filter(e => e.role === p && fits(e));
31
- return ofRole.length
32
- ? [{ group: `kit · ${roleLabel(p)}`, options: ofRole.map(e => ({ value: e.name, label: desc(e) })) }]
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: the video's and the kit's sounds, to drag onto the timeline or pick with a click
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, roleLabel, round2 } from "../../../../lib/sound-timeline";
8
+ import { effectSrc, fileName, kitLibrary, round2 } from "../../../../lib/sound-timeline";
8
9
  import { AddSound } from "./AddSound";
9
- import type { EffectInfo, SoundCatalog } from "./types";
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 f = filter.trim().toLowerCase();
28
- const passes = (e: EffectInfo) =>
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
- <h5>
55
- {name} <span className="subtle">{l.length}</span>
56
- </h5>
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) && <p className="empty">Nenhum som{f ? " com esse filtro" : ""}</p>}
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
  );
@@ -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
+ }