@crievo/omni 0.1.2 → 0.1.4

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 (122) hide show
  1. package/docs/onboarding.md +69 -161
  2. package/omnivideo.config.example.json +1 -1
  3. package/package.json +2 -1
  4. package/reference/learnings.md +12 -0
  5. package/reference/omnichat.md +5 -4
  6. package/reference/publish.md +9 -9
  7. package/reference/studio.md +1 -1
  8. package/studio/api/app.ts +2 -2
  9. package/studio/api/routes/sound.ts +22 -3
  10. package/studio/api/services/onboarding.ts +242 -21
  11. package/studio/api/services/settings.ts +4 -2
  12. package/studio/api/services/sound.ts +45 -0
  13. package/studio/shared/types/api.ts +26 -1
  14. package/studio/studio.ts +1 -0
  15. package/studio/web/components/AppShell.tsx +3 -1
  16. package/studio/web/components/CloneVoiceDialog/RecordStep.tsx +3 -0
  17. package/studio/web/components/CloneVoiceDialog/RecorderBar.tsx +37 -20
  18. package/studio/web/components/CloneVoiceDialog/useCloneVoice.ts +39 -12
  19. package/studio/web/components/MissingDmsPanel/BatchSend.tsx +13 -4
  20. package/studio/web/components/Omni/OmniWidget.tsx +14 -5
  21. package/studio/web/components/Omni/PendingActions.tsx +25 -6
  22. package/studio/web/components/OmnichatRulePanel/RuleConfig.tsx +13 -3
  23. package/studio/web/components/OmnichatRulePanel/RuleEnableConfirm.tsx +14 -4
  24. package/studio/web/components/ResultMessage.tsx +12 -3
  25. package/studio/web/components/Sidebar.tsx +63 -24
  26. package/studio/web/components/StageStepper.tsx +105 -34
  27. package/studio/web/components/Toaster.tsx +25 -0
  28. package/studio/web/components/VoiceCarousel.tsx +19 -16
  29. package/studio/web/components/VoiceProfilesSettings.tsx +8 -3
  30. package/studio/web/components/bencho/ActionNode.tsx +456 -0
  31. package/studio/web/components/bencho/AssigneePicker.tsx +246 -0
  32. package/studio/web/components/bencho/BenchoScope.tsx +8 -0
  33. package/studio/web/components/bencho/Checklist.tsx +421 -0
  34. package/studio/web/components/bencho/CreateMenu.tsx +258 -0
  35. package/studio/web/components/bencho/GenerateImage.tsx +290 -0
  36. package/studio/web/components/bencho/ImageAccordion.tsx +273 -0
  37. package/studio/web/components/bencho/LiquidToggle.tsx +289 -0
  38. package/studio/web/components/bencho/MagnifyDock.tsx +200 -0
  39. package/studio/web/components/bencho/NowPlaying.tsx +403 -0
  40. package/studio/web/components/bencho/OneTimeCode.tsx +374 -0
  41. package/studio/web/components/bencho/PullToRefresh.tsx +527 -0
  42. package/studio/web/components/bencho/SlideToConfirm.tsx +385 -0
  43. package/studio/web/components/bencho/SloshSlider.tsx +394 -0
  44. package/studio/web/components/bencho/StackedBars.tsx +342 -0
  45. package/studio/web/components/bencho/ToastStack.tsx +263 -0
  46. package/studio/web/components/bencho/UploadDropzone.tsx +394 -0
  47. package/studio/web/components/bencho/VoiceNote.tsx +705 -0
  48. package/studio/web/components/icons.tsx +27 -0
  49. package/studio/web/components/ui/slider.tsx +34 -1
  50. package/studio/web/components/ui/switch.tsx +18 -5
  51. package/studio/web/hooks/useOnboarding.ts +24 -6
  52. package/studio/web/index.html +1 -1
  53. package/studio/web/lib/alerts.ts +10 -0
  54. package/studio/web/lib/omni-panel.ts +4 -1
  55. package/studio/web/lib/toast.ts +196 -0
  56. package/studio/web/lib/upload.ts +24 -0
  57. package/studio/web/pages/BenchoPage/index.tsx +74 -0
  58. package/studio/web/pages/BenchoPage/registry.ts +174 -0
  59. package/studio/web/pages/GettingStartedPage/components/ComputerCard.tsx +102 -0
  60. package/studio/web/pages/GettingStartedPage/index.tsx +84 -58
  61. package/studio/web/pages/NewVideoPage/index.tsx +6 -2
  62. package/studio/web/pages/ReferencesPage/index.tsx +4 -1
  63. package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +30 -3
  64. package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +14 -17
  65. package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +1 -1
  66. package/studio/web/pages/VideoPage/components/PublishForm.tsx +24 -17
  67. package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +2 -1
  68. package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +12 -14
  69. package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +27 -11
  70. package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +22 -18
  71. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +3 -4
  72. package/studio/web/pages/VideoPage/components/SoundTimeline/UploadTrack.tsx +59 -0
  73. package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +1 -0
  74. package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +4 -6
  75. package/studio/web/pages/VideoPage/index.tsx +4 -0
  76. package/studio/web/pages/VideosPage/components/VideoRow.tsx +2 -4
  77. package/studio/web/router.tsx +8 -0
  78. package/studio/web/styles/components/bencho/action-node.css +227 -0
  79. package/studio/web/styles/components/bencho/checklist.css +182 -0
  80. package/studio/web/styles/components/bencho/dock.css +188 -0
  81. package/studio/web/styles/components/bencho/gallery.css +109 -0
  82. package/studio/web/styles/components/bencho/generate.css +151 -0
  83. package/studio/web/styles/components/bencho/image-accordion.css +37 -0
  84. package/studio/web/styles/components/bencho/liq-create.css +279 -0
  85. package/studio/web/styles/components/bencho/liq-toggle.css +106 -0
  86. package/studio/web/styles/components/bencho/one-time-code.css +148 -0
  87. package/studio/web/styles/components/bencho/picker.css +204 -0
  88. package/studio/web/styles/components/bencho/pull.css +313 -0
  89. package/studio/web/styles/components/bencho/scope.css +42 -0
  90. package/studio/web/styles/components/bencho/slide-confirm.css +139 -0
  91. package/studio/web/styles/components/bencho/slosh.css +137 -0
  92. package/studio/web/styles/components/bencho/sound.css +260 -0
  93. package/studio/web/styles/components/bencho/stacked-bars.css +334 -0
  94. package/studio/web/styles/components/bencho/toast-stack.css +192 -0
  95. package/studio/web/styles/components/bencho/tokens.css +243 -0
  96. package/studio/web/styles/components/bencho/upload-dropzone.css +352 -0
  97. package/studio/web/styles/components/bencho/voice-note.css +337 -0
  98. package/studio/web/styles/components/clone-voice.css +12 -2
  99. package/studio/web/styles/components/omni.css +6 -1
  100. package/studio/web/styles/components/stepper.css +123 -7
  101. package/studio/web/styles/components/toaster.css +10 -0
  102. package/studio/web/styles/components/ui/slider.css +6 -16
  103. package/studio/web/styles/components/ui/switch.css +2 -41
  104. package/studio/web/styles/components/voice-carousel.css +46 -4
  105. package/studio/web/styles/index.css +23 -0
  106. package/studio/web/styles/pages/getting-started.css +46 -0
  107. package/studio/web/styles/pages/settings/shell.css +4 -0
  108. package/studio/web/styles/pages/video/layout.css +1 -4
  109. package/studio/web/styles/pages/video/sound-timeline.css +8 -0
  110. package/studio/web/styles/theme/identity/shell.css +10 -26
  111. package/tools/lib/agents.mjs +24 -13
  112. package/tools/lib/claude-settings.mjs +32 -3
  113. package/tools/lib/deps.mjs +24 -0
  114. package/tools/lib/platforms/instagram.mjs +1 -1
  115. package/tools/lib/up.mjs +5 -2
  116. package/tools/omni.mjs +7 -5
  117. package/tools/omnichat.mjs +5 -7
  118. package/tools/package.mjs +5 -3
  119. package/tools/publish.mjs +40 -137
  120. package/tools/reference.mjs +1 -1
  121. package/tools/setup.mjs +16 -15
  122. package/tools/sound.mjs +67 -3
@@ -0,0 +1,174 @@
1
+ /* The Bencho ports shown on the /bencho gallery: one entry per component (components/bencho/<Name>.tsx, styles in
2
+ styles/components/bencho/<id>.css). `id` is the block's id on bencho.dev; `fill` and `glass` are what the site's stage
3
+ gives it (the card color it draws with and whether it reads the glass tokens). */
4
+
5
+ import { lazy, type ComponentType, type LazyExoticComponent } from "react";
6
+
7
+ export type BenchoEntry = {
8
+ id: string;
9
+ title: string;
10
+ description: string;
11
+ category: string;
12
+ fill: boolean;
13
+ glass: boolean;
14
+ Component: LazyExoticComponent<ComponentType>;
15
+ };
16
+
17
+ const load = (loader: () => Promise<Record<string, ComponentType>>, name: string) =>
18
+ lazy(() => loader().then(m => ({ default: m[name] as ComponentType })));
19
+
20
+ export const BENCHO: BenchoEntry[] = [
21
+ {
22
+ id: "pull",
23
+ title: "Puxar para atualizar",
24
+ description: "Puxe o cartão para baixo: ele estica e, passando da linha, solta e atualiza.",
25
+ category: "Drag",
26
+ fill: true,
27
+ glass: false,
28
+ Component: load(() => import("../../components/bencho/PullToRefresh"), "PullToRefresh"),
29
+ },
30
+ {
31
+ id: "liq-toggle",
32
+ title: "Switch líquido",
33
+ description: "Uma gota que se alonga ao cruzar o trilho; clique ou arraste.",
34
+ category: "Press",
35
+ fill: false,
36
+ glass: true,
37
+ Component: load(() => import("../../components/bencho/LiquidToggle"), "LiquidToggle"),
38
+ },
39
+ {
40
+ id: "checklist",
41
+ title: "Lista de tarefas",
42
+ description: "A caixa enche, o check se desenha e o texto é riscado.",
43
+ category: "Press",
44
+ fill: true,
45
+ glass: true,
46
+ Component: load(() => import("../../components/bencho/Checklist"), "Checklist"),
47
+ },
48
+ {
49
+ id: "sound",
50
+ title: "Tocando agora",
51
+ description: "Uma barra de reprodução que abre em player completo e volta como um só objeto.",
52
+ category: "Press",
53
+ fill: true,
54
+ glass: true,
55
+ Component: load(() => import("../../components/bencho/NowPlaying"), "NowPlaying"),
56
+ },
57
+ {
58
+ id: "dock",
59
+ title: "Dock com lupa",
60
+ description: "Os ícones crescem em direção ao cursor, o mais próximo cresce mais.",
61
+ category: "Hover",
62
+ fill: true,
63
+ glass: true,
64
+ Component: load(() => import("../../components/bencho/MagnifyDock"), "MagnifyDock"),
65
+ },
66
+ {
67
+ id: "slosh",
68
+ title: "Slider líquido",
69
+ description: "O preenchimento tem peso: arraste rápido e ele balança além da alça.",
70
+ category: "Slide",
71
+ fill: true,
72
+ glass: false,
73
+ Component: load(() => import("../../components/bencho/SloshSlider"), "SloshSlider"),
74
+ },
75
+ {
76
+ id: "liq-create",
77
+ title: "Menu de criar",
78
+ description: "Aperte a pílula e ela se abre no menu de criar.",
79
+ category: "Press",
80
+ fill: true,
81
+ glass: false,
82
+ Component: load(() => import("../../components/bencho/CreateMenu"), "CreateMenu"),
83
+ },
84
+ {
85
+ id: "picker",
86
+ title: "Atribuir pessoas",
87
+ description: "Escolha pessoas numa lista e os rostos se empilham na pílula.",
88
+ category: "Select",
89
+ fill: true,
90
+ glass: true,
91
+ Component: load(() => import("../../components/bencho/AssigneePicker"), "AssigneePicker"),
92
+ },
93
+ {
94
+ id: "slide-confirm",
95
+ title: "Deslizar para confirmar",
96
+ description: "Empurre a alça; passando da marca, ela termina sozinha.",
97
+ category: "Drag",
98
+ fill: true,
99
+ glass: true,
100
+ Component: load(() => import("../../components/bencho/SlideToConfirm"), "SlideToConfirm"),
101
+ },
102
+ {
103
+ id: "image-accordion",
104
+ title: "Acordeão de imagens",
105
+ description: "Passe o mouse numa imagem e ela abre enquanto as vizinhas cedem.",
106
+ category: "Hover",
107
+ fill: false,
108
+ glass: false,
109
+ Component: load(() => import("../../components/bencho/ImageAccordion"), "ImageAccordion"),
110
+ },
111
+ {
112
+ id: "action-node",
113
+ title: "Nó de ação",
114
+ description: "Um cartão de etapa com os controles dobrados no canto.",
115
+ category: "Hover",
116
+ fill: true,
117
+ glass: true,
118
+ Component: load(() => import("../../components/bencho/ActionNode"), "ActionNode"),
119
+ },
120
+ {
121
+ id: "generate",
122
+ title: "Gerar imagem",
123
+ description: "Aperte Gerar e a imagem se resolve sob o vidro, das formas largas aos detalhes.",
124
+ category: "Press",
125
+ fill: true,
126
+ glass: false,
127
+ Component: load(() => import("../../components/bencho/GenerateImage"), "GenerateImage"),
128
+ },
129
+ {
130
+ id: "one-time-code",
131
+ title: "Código de uso único",
132
+ description: "Vários campos que são um só: colar, preencher e apagar funcionam.",
133
+ category: "Type",
134
+ fill: true,
135
+ glass: false,
136
+ Component: load(() => import("../../components/bencho/OneTimeCode"), "OneTimeCode"),
137
+ },
138
+ {
139
+ id: "upload-dropzone",
140
+ title: "Envio por arrastar",
141
+ description: "Arraste um arquivo para a caixa; um grande demais é recusado.",
142
+ category: "Drag",
143
+ fill: true,
144
+ glass: false,
145
+ Component: load(() => import("../../components/bencho/UploadDropzone"), "UploadDropzone"),
146
+ },
147
+ {
148
+ id: "voice-note",
149
+ title: "Nota de voz",
150
+ description: "Segure para gravar a onda, deslize para a esquerda para descartar, solte para ouvir.",
151
+ category: "Press",
152
+ fill: true,
153
+ glass: false,
154
+ Component: load(() => import("../../components/bencho/VoiceNote"), "VoiceNote"),
155
+ },
156
+ {
157
+ id: "toast-stack",
158
+ title: "Pilha de avisos",
159
+ description: "Três avisos empilhados; aponte e eles se abrem em lista.",
160
+ category: "Hover",
161
+ fill: true,
162
+ glass: false,
163
+ Component: load(() => import("../../components/bencho/ToastStack"), "ToastStack"),
164
+ },
165
+ {
166
+ id: "stacked-bars",
167
+ title: "Barras empilhadas",
168
+ description: "Um mês de visitas em barras; um dia é destacado e o tooltip desliza junto.",
169
+ category: "Hover",
170
+ fill: true,
171
+ glass: false,
172
+ Component: load(() => import("../../components/bencho/StackedBars"), "StackedBars"),
173
+ },
174
+ ];
@@ -0,0 +1,102 @@
1
+ /* "Este computador", on top of Getting started: what `omni up` is still preparing on this Mac, live.
2
+ Being on the page already means the login and the studio connection work (the hosted studio only
3
+ answers through this computer's runner). The four programs come from checkDeps: while one is
4
+ missing it reads "preparando…" (the CLI is downloading it; nothing for the user to do). The skills'
5
+ permission rules are written by Claude, on the user's yes, through the plugin's MCP: the studio only
6
+ says what to ask for. The Claude Code account is looked for where the plugin's hook found it, else
7
+ ~/.claude; without a login there, the user enters or gives the folder in Settings. */
8
+
9
+ import { Link } from "@tanstack/react-router";
10
+ import { Badge } from "../../../components/ui/badge";
11
+ import { Markdown } from "../../../components/Markdown";
12
+ import { Check, X } from "../../../components/icons";
13
+ import { useRunner } from "../../../hooks/useRunner";
14
+ import type { OnboardingComputer } from "../../../types/api";
15
+
16
+ type Row = { id: string; title: string; value: string | null; detail?: string };
17
+
18
+ export function ComputerCard({ computer: c, markdown }: { computer: OnboardingComputer; markdown: string }) {
19
+ const { hosted, name } = useRunner();
20
+ const rulesOk = c.dev || c.missing_rules.length === 0;
21
+ const programs: Row[] = [
22
+ { id: "ffmpeg", title: "ffmpeg", value: c.ffmpeg },
23
+ { id: "ffprobe", title: "ffprobe", value: c.ffprobe },
24
+ { id: "chrome", title: "Chrome sem janela", value: c.chrome },
25
+ { id: "python", title: "Python das ferramentas", value: c.python },
26
+ { id: "ytdlp", title: "yt-dlp", value: c.ytdlp },
27
+ ];
28
+ const state = !c.ready ? "preparing" : !c.agent.ok ? "agent" : rulesOk ? "ok" : "rules";
29
+ return (
30
+ <section className={`onboarding-computer ${state}`} aria-live="polite" aria-labelledby="computer-title">
31
+ <header>
32
+ <h2 id="computer-title">Este computador</h2>
33
+ {state === "ok" && <Badge variant="ok">Computador pronto</Badge>}
34
+ {state === "rules" && <Badge variant="warning">Falta liberar as skills</Badge>}
35
+ {state === "agent" && <Badge variant="warning">Falta a conta do Claude Code</Badge>}
36
+ {state === "preparing" && (
37
+ <Badge variant="running">
38
+ <span className="spinning" aria-hidden="true" /> Preparando…
39
+ </Badge>
40
+ )}
41
+ </header>
42
+ {state === "preparing" && (
43
+ <p className="subtle">
44
+ O OmniVideo está baixando o que falta. Leva alguns minutos e não precisa fazer nada: siga pelos passos abaixo
45
+ enquanto isso.
46
+ </p>
47
+ )}
48
+ <ul className="onboarding-items">
49
+ <li className="ok">
50
+ <span className="onboarding-item-mark">
51
+ <Check weight="bold" />
52
+ </span>
53
+ <b>Login e studio conectados</b>
54
+ <span className="subtle">
55
+ {hosted ? (name ? `conectado pelo ${name}` : "conectado") : "studio aberto neste computador"}
56
+ </span>
57
+ </li>
58
+ {programs.map(p => (
59
+ <li key={p.id} className={p.value ? "ok" : "preparing"}>
60
+ <span className="onboarding-item-mark">
61
+ {p.value ? <Check weight="bold" /> : <span className="spinning" aria-hidden="true" />}
62
+ </span>
63
+ <b>{p.title}</b>
64
+ <span className="subtle">{p.value ?? "preparando…"}</span>
65
+ </li>
66
+ ))}
67
+ <li className={c.agent.ok ? "ok" : "missing"}>
68
+ <span className="onboarding-item-mark">{c.agent.ok ? <Check weight="bold" /> : <X weight="bold" />}</span>
69
+ <b>Conta do Claude Code</b>
70
+ <span className="subtle">{c.agent.detail}</span>
71
+ </li>
72
+ <li className={rulesOk ? "ok" : "missing"}>
73
+ <span className="onboarding-item-mark">{rulesOk ? <Check weight="bold" /> : <X weight="bold" />}</span>
74
+ <b>Permissão das skills no Claude Code</b>
75
+ <span className="subtle">
76
+ {c.missing_rules.length === 0
77
+ ? "liberadas"
78
+ : c.dev
79
+ ? "repositório de desenvolvimento: as skills são as dele"
80
+ : "ainda não liberadas"}
81
+ </span>
82
+ </li>
83
+ </ul>
84
+ {!c.agent.ok && (
85
+ <p className="onboarding-ask">
86
+ Não achei o login do seu Claude Code. Se ele usa outra pasta, coloque ela em{" "}
87
+ <Link to="/settings/$category" params={{ category: "agent" }}>
88
+ Configurações → Agente de IA
89
+ </Link>
90
+ ; se é essa mesma, entre por lá.
91
+ </p>
92
+ )}
93
+ {!rulesOk && (
94
+ <p className="onboarding-ask">
95
+ No Claude Code, diga: <q>libere as skills do OmniVideo</q>. O Claude mostra as regras, pede o seu ok e grava.
96
+ Esta tela atualiza sozinha.
97
+ </p>
98
+ )}
99
+ {markdown && <Markdown text={markdown} />}
100
+ </section>
101
+ );
102
+ }
@@ -1,50 +1,22 @@
1
- /* Getting started: the docs/onboarding.md guide, one step per section, each with what the machine
2
- already has (tools/setup.mjs check) and the shortcut to the screen where it gets solved. The text
3
- is the guide's: changed there, changes here. Opens on the first missing step. */
1
+ /* Getting started: the docs/onboarding.md guide, one step per section. On top, "Este computador": what
2
+ the CLI is still preparing on this Mac, live (the page asks again every 5 s until it's ready). Below,
3
+ the steps that keep the user busy meanwhile (voice, platforms, style, first video), each with its
4
+ state and the shortcut to the screen where it gets solved. The text is the guide's: changed there,
5
+ changes here. Opens on the first missing step. */
4
6
 
5
7
  import { useEffect, useState, type ReactNode } from "react";
6
- import { Link } from "@tanstack/react-router";
8
+ import { Link, useNavigate } from "@tanstack/react-router";
7
9
  import { useOnboarding } from "../../hooks/useOnboarding";
10
+ import { useSettings } from "../../hooks/useSettings";
11
+ import { invalidate, queryKeys } from "../../lib/query-client";
8
12
  import { Badge } from "../../components/ui/badge";
9
13
  import { Skeleton } from "../../components/ui/skeleton";
10
14
  import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "../../components/ui/accordion";
11
15
  import { Markdown } from "../../components/Markdown";
12
- import { ArrowClockwise, Check, Gear, Microphone, Plug, Plus, Sparkle, Stack, X } from "../../components/icons";
16
+ import { CloneVoiceDialog } from "../../components/CloneVoiceDialog";
17
+ import { ArrowClockwise, Check, Gear, Microphone, Plug, Plus, Stack, X } from "../../components/icons";
13
18
  import type { OnboardingCheck, OnboardingStep } from "../../types/api";
14
-
15
- /* where each step gets solved in the studio */
16
- const STEP_ACTIONS: Record<string, ReactNode> = {
17
- claude: (
18
- <Link to="/settings/$category" params={{ category: "agent" }} className="button">
19
- <Sparkle className="ic" /> Configurações → Agente de IA
20
- </Link>
21
- ),
22
- voice: (
23
- <>
24
- <Link to="/settings/$category" params={{ category: "providers" }} className="button">
25
- <Gear className="ic" /> Provedores de voz
26
- </Link>
27
- <Link to="/settings/$category" params={{ category: "voice" }} className="button">
28
- <Microphone className="ic" /> Clonar a minha voz
29
- </Link>
30
- </>
31
- ),
32
- reference: (
33
- <Link to="/styles" className="button">
34
- <Stack className="ic" /> Abrir Estilos visuais
35
- </Link>
36
- ),
37
- "first-video": (
38
- <Link to="/new" className="button primary">
39
- <Plus className="ic" /> Criar vídeo
40
- </Link>
41
- ),
42
- publish: (
43
- <Link to="/settings/$category" params={{ category: "platforms" }} className="button">
44
- <Plug className="ic" /> Conectar as plataformas
45
- </Link>
46
- ),
47
- };
19
+ import { ComputerCard } from "./components/ComputerCard";
48
20
 
49
21
  const STEP_MARK: Record<OnboardingCheck["state"], ReactNode> = {
50
22
  ok: <Check weight="bold" />,
@@ -54,8 +26,9 @@ const STEP_MARK: Record<OnboardingCheck["state"], ReactNode> = {
54
26
 
55
27
  function stepBadge(s: OnboardingStep) {
56
28
  if (s.state === "ok") return <Badge variant="ok">Pronto</Badge>;
29
+ if (s.optional) return <Badge variant="neutral">Opcional</Badge>;
57
30
  if (s.state === "missing") return <Badge variant="error">Falta</Badge>;
58
- if (s.state === "warning") return <Badge variant="warning">{s.optional ? "Opcional" : "Atenção"}</Badge>;
31
+ if (s.state === "warning") return <Badge variant="warning">Atenção</Badge>;
59
32
  return null;
60
33
  }
61
34
 
@@ -64,17 +37,60 @@ const withoutImages = (md: string) => md.replace(/!\[[^\]]*\]\([^)]*\)\n?/g, "")
64
37
 
65
38
  export function GettingStartedPage() {
66
39
  const { onboarding: d, error: loadError, checking, reload } = useOnboarding();
40
+ const { settings, reload: reloadSettings } = useSettings();
41
+ const navigate = useNavigate();
42
+ const [cloning, setCloning] = useState(false);
67
43
  const [open, setOpen] = useState<string>("");
44
+ const computerStep = d?.steps.find(s => s.id === "computer");
45
+ const steps = d?.steps.filter(s => s.id !== "computer") ?? [];
68
46
  /* opens on the first missing step; checking again doesn't close what you opened */
69
47
  useEffect(() => {
70
- if (d) setOpen(o => o || (d.steps.find(s => s.state === "missing")?.id ?? ""));
48
+ if (d)
49
+ setOpen(o => o || (d.steps.find(s => s.id !== "computer" && !s.optional && s.state === "missing")?.id ?? ""));
71
50
  }, [d]);
51
+ useEffect(() => {
52
+ document.title = "Primeiros passos · Omni";
53
+ }, []);
72
54
  const error = loadError ?? d?.error ?? null;
73
55
 
74
56
  const required = d?.steps.filter(s => !s.optional) ?? [];
75
57
  const ready = required.filter(s => s.state === "ok" || s.state === "warning").length;
76
58
  const pct = required.length ? Math.round((ready / required.length) * 100) : 0;
77
59
 
60
+ /* where each step gets solved in the studio */
61
+ const actions: Record<string, ReactNode> = {
62
+ "voice-key": (
63
+ <Link to="/settings/$category" params={{ category: "providers" }} className="button">
64
+ <Gear className="ic" /> Provedores de voz
65
+ </Link>
66
+ ),
67
+ "voice-clone": (
68
+ <button type="button" className="button" onClick={() => setCloning(true)} disabled={!settings}>
69
+ <Microphone className="ic" /> Clonar a minha voz
70
+ </button>
71
+ ),
72
+ "voice-default": (
73
+ <Link to="/voice" className="button">
74
+ <Microphone className="ic" /> Perfil de voz
75
+ </Link>
76
+ ),
77
+ platforms: (
78
+ <Link to="/settings/$category" params={{ category: "platforms" }} className="button">
79
+ <Plug className="ic" /> Conectar as plataformas
80
+ </Link>
81
+ ),
82
+ style: (
83
+ <Link to="/styles" className="button">
84
+ <Stack className="ic" /> Abrir Estilos visuais
85
+ </Link>
86
+ ),
87
+ "first-video": (
88
+ <Link to="/new" className="button primary">
89
+ <Plus className="ic" /> Criar vídeo
90
+ </Link>
91
+ ),
92
+ };
93
+
78
94
  return (
79
95
  <div className="page onboarding">
80
96
  <header className="header">
@@ -87,7 +103,7 @@ export function GettingStartedPage() {
87
103
  </span>
88
104
  )}
89
105
  <span className="push-end" />
90
- <button className="button" onClick={() => reload(true)} disabled={checking}>
106
+ <button type="button" className="button" onClick={() => reload(true)} disabled={checking}>
91
107
  <ArrowClockwise className="ic" /> {checking ? "Conferindo…" : "Conferir de novo"}
92
108
  </button>
93
109
  </header>
@@ -106,37 +122,33 @@ export function GettingStartedPage() {
106
122
  {!d && !error && (
107
123
  <div
108
124
  aria-busy="true"
109
- aria-label="Conferindo a máquina"
125
+ aria-label="Conferindo o computador"
110
126
  style={{ display: "flex", flexDirection: "column", gap: 10 }}
111
127
  >
112
128
  <Skeleton className="row" style={{ width: "70%" }} />
113
- <Skeleton className="row" style={{ width: "55%" }} />
114
- {Array.from({ length: 8 }, (_, i) => (
115
- <Skeleton key={i} style={{ height: 48, width: "100%", marginTop: i ? 0 : 6 }} />
129
+ <Skeleton style={{ height: 180, width: "100%", marginTop: 6 }} />
130
+ {Array.from({ length: 6 }, (_, i) => (
131
+ <Skeleton key={i} style={{ height: 48, width: "100%" }} />
116
132
  ))}
117
133
  </div>
118
134
  )}
119
135
  {d && (
120
136
  <>
121
137
  <Markdown text={withoutImages(d.intro)} />
138
+ {d.computer && <ComputerCard computer={d.computer} markdown={computerStep?.markdown ?? ""} />}
122
139
  {d.ready && (
123
140
  <div className="onboarding-ready">
124
141
  <Check weight="bold" className="ic" />
125
142
  <span>
126
- <b>Tudo pronto para o primeiro vídeo.</b> Publicar nas redes fica para depois, no passo 8.
143
+ <b>Tudo pronto para o primeiro vídeo.</b> Conectar as redes pode ficar para depois.
127
144
  </span>
128
145
  <Link to="/new" className="button primary">
129
146
  <Plus className="ic" /> Criar vídeo
130
147
  </Link>
131
148
  </div>
132
149
  )}
133
- {!d.checked && (
134
- <p className="warning">
135
- Não deu para conferir a máquina (tools/setup.mjs check). Os passos abaixo valem do mesmo jeito.
136
- </p>
137
- )}
138
150
  <Accordion type="single" collapsible value={open} onValueChange={setOpen} className="onboarding-steps">
139
- {d.steps.map((s, i) => (
151
+ {steps.map((s, i) => (
140
152
  <AccordionItem key={s.id} value={s.id} className={`onboarding-step ${s.state ?? ""}`}>
141
153
  <AccordionTrigger>
142
154
  <span className={`onboarding-mark ${s.state ?? ""}`}>
@@ -157,18 +169,32 @@ export function GettingStartedPage() {
157
169
  ))}
158
170
  </ul>
159
171
  )}
160
- {STEP_ACTIONS[s.id] && <div className="actions onboarding-actions">{STEP_ACTIONS[s.id]}</div>}
172
+ {actions[s.id] && <div className="actions onboarding-actions">{actions[s.id]}</div>}
161
173
  <Markdown text={s.markdown} />
162
174
  </AccordionContent>
163
175
  </AccordionItem>
164
176
  ))}
165
177
  </Accordion>
166
- <p className="hint">
167
- Este guia é o <code>docs/onboarding.md</code> do repositório. No terminal,{" "}
168
- <code>node tools/setup.mjs check</code> faz a mesma conferência.
169
- </p>
170
178
  </>
171
179
  )}
180
+ {cloning && settings && (
181
+ <CloneVoiceDialog
182
+ settings={settings}
183
+ reload={reloadSettings}
184
+ onClose={() => setCloning(false)}
185
+ onAlreadyHave={() => {
186
+ setCloning(false);
187
+ void navigate({ to: "/voice" });
188
+ }}
189
+ onCreate={warnings => {
190
+ setCloning(false);
191
+ reloadSettings();
192
+ void invalidate(queryKeys.onboarding);
193
+ /* a provider that didn't create the clone gets fixed in the voice's editor */
194
+ if (warnings.length) void navigate({ to: "/voice" });
195
+ }}
196
+ />
197
+ )}
172
198
  </div>
173
199
  );
174
200
  }
@@ -3,7 +3,7 @@
3
3
  scene approvals); carousel goes up to the export; both, the video first and its carousel after. */
4
4
 
5
5
  import { useState } from "react";
6
- import { Link, useNavigate } from "@tanstack/react-router";
6
+ import { Link, useLocation, useNavigate } from "@tanstack/react-router";
7
7
  import { api, send } from "../../lib/api";
8
8
  import { useReferences } from "../../hooks/useReferences";
9
9
  import { useVoiceProfiles } from "../../hooks/useVoiceProfiles";
@@ -20,7 +20,11 @@ export function NewVideoPage() {
20
20
  const { references } = useReferences();
21
21
  const [request, setRequest] = useState("");
22
22
  const [format, setFormat] = useState("9:16");
23
- const [deliverables, setDeliverables] = useState<Deliverable[]>(["video"]);
23
+ /* the sidebar's Criar menu opens this form with the carousel already checked (router history state, typed in Sidebar.tsx) */
24
+ const asked = useLocation({ select: l => l.state.deliverables });
25
+ const [deliverables, setDeliverables] = useState<Deliverable[]>(
26
+ asked?.length && asked.every(d => d === "video" || d === "carousel") ? asked : ["video"],
27
+ );
24
28
  const hasVideo = deliverables.includes("video");
25
29
  const [reference, setReference] = useState("");
26
30
  const { profiles, defaultId, reload: reloadProfiles } = useVoiceProfiles();
@@ -3,6 +3,7 @@
3
3
  which becomes an agent session (measure and write the guide). */
4
4
 
5
5
  import { useEffect, useState } from "react";
6
+ import { useLocation } from "@tanstack/react-router";
6
7
  import { useReferences } from "../../hooks/useReferences";
7
8
  import { Plus } from "../../components/icons";
8
9
  import { NewReferenceForm } from "./components/NewReferenceForm";
@@ -11,7 +12,9 @@ import { ReferenceGridSkeleton } from "./components/ReferenceGridSkeleton";
11
12
 
12
13
  export function ReferencesPage() {
13
14
  const { references, error } = useReferences();
14
- const [creating, setCreating] = useState(false);
15
+ /* the sidebar's Criar menu comes here with the new-style form already open (router history state, typed in Sidebar.tsx) */
16
+ const asked = useLocation({ select: l => l.state.creating === true });
17
+ const [creating, setCreating] = useState(asked);
15
18
  useEffect(() => {
16
19
  document.title = "Estilos visuais · Omni";
17
20
  }, []);
@@ -1,6 +1,7 @@
1
1
  /* The agent accounts (agents in the config, tools/lib/agents.mjs): N Claude dirs and N Codex homes in
2
2
  rotation order. Each new session goes to the next free one; an account over its limit is set aside
3
- until it resets and the session carries on in the next. ~/.claude is refused by the server. */
3
+ until it resets and the session carries on in the next. With no list saved, the one account is the Claude
4
+ Code this machine uses; when it has no login there, the screen asks for its folder. */
4
5
 
5
6
  import { useEffect, useState } from "react";
6
7
  import { useSaveAction } from "../../../hooks/useSaveAction";
@@ -16,7 +17,7 @@ import { Switch } from "../../../components/ui/switch";
16
17
  type Draft = Pick<AgentAccount, "id" | "provider" | "dir" | "label" | "enabled">;
17
18
 
18
19
  const PROVIDER: Record<AgentProvider, string> = { claude: "Claude", codex: "Codex" };
19
- const SUGGESTED_DIR: Record<AgentProvider, string> = { claude: "~/.claude-personal", codex: "~/.codex" };
20
+ const SUGGESTED_DIR: Record<AgentProvider, string> = { claude: "~/.claude", codex: "~/.codex" };
20
21
 
21
22
  const at = (ms: number) =>
22
23
  new Date(ms).toLocaleString("pt-BR", { weekday: "short", hour: "2-digit", minute: "2-digit" });
@@ -34,6 +35,7 @@ export function AgentAccounts({ agents, onSave }: { agents: AgentsSettings; onSa
34
35
  const [list, setList] = useState(initial);
35
36
  const [adding, setAdding] = useState<AgentProvider | null>(null);
36
37
  const [newDir, setNewDir] = useState("");
38
+ const [claudeDir, setClaudeDir] = useState("");
37
39
  const { result, saving, save } = useSaveAction(onSave);
38
40
  const login = useSaveAction(onSave);
39
41
  useEffect(() => setList(initial()), [JSON.stringify(agents.accounts)]);
@@ -78,6 +80,13 @@ export function AgentAccounts({ agents, onSave }: { agents: AgentsSettings; onSa
78
80
  const ok = await store([...list, { id: "", provider: adding, dir: newDir.trim(), label: "", enabled: true }]);
79
81
  if (ok) setAdding(null);
80
82
  };
83
+ /* the default account without a login: the user's Claude Code lives in another folder */
84
+ const own = agents.accounts.length === 1 && agents.accounts[0].implicit && !agents.accounts[0].login;
85
+ const useClaudeDir = async () => {
86
+ if (!claudeDir.trim()) return;
87
+ const ok = await store([{ id: "", provider: "claude", dir: claudeDir.trim(), label: "", enabled: true }]);
88
+ if (ok) setClaudeDir("");
89
+ };
81
90
 
82
91
  return (
83
92
  <SettingRow
@@ -90,6 +99,24 @@ export function AgentAccounts({ agents, onSave }: { agents: AgentsSettings; onSa
90
99
  </>
91
100
  }
92
101
  >
102
+ {own && (
103
+ <div className="setting-row agent-add agent-missing">
104
+ <small className="warning">
105
+ Não achei o login do Claude Code em <code>{agents.accounts[0].dir}</code>. Se ele usa outra pasta (a do{" "}
106
+ <code>CLAUDE_CONFIG_DIR</code>), coloque ela aqui; se é essa mesma, use Entrar.
107
+ </small>
108
+ <input
109
+ className="mono"
110
+ value={claudeDir}
111
+ onChange={e => setClaudeDir(e.target.value)}
112
+ aria-label="Pasta do Claude Code"
113
+ placeholder="~/.claude-outra"
114
+ />
115
+ <button className="button primary" disabled={saving || !claudeDir.trim()} onClick={useClaudeDir}>
116
+ Usar esta pasta
117
+ </button>
118
+ </div>
119
+ )}
93
120
  <div className="claude-options agent-accounts">
94
121
  {agents.accounts.map((a, i) => {
95
122
  const limited = a.limited_until && a.limited_until > Date.now();
@@ -204,7 +231,7 @@ export function AgentAccounts({ agents, onSave }: { agents: AgentsSettings; onSa
204
231
  )}
205
232
  <p className="hint">
206
233
  Cada conta é uma pasta com o próprio login (<code>CLAUDE_CONFIG_DIR</code> no Claude, <code>CODEX_HOME</code> no
207
- Codex). O <code>~/.claude</code>, a conta padrão do Claude Code, é proibido aqui.
234
+ Codex). Sem nenhuma conta salva, vale o Claude Code deste computador.
208
235
  {!agents.codex.installed && " O Codex não está instalado: instale o app do ChatGPT ou o codex no PATH."}
209
236
  </p>
210
237
  <ResultMessage result={result ?? login.result} />