diff --git a/src/backend/models.ts b/src/backend/models.ts index 6e8d6478..f97a6655 100644 --- a/src/backend/models.ts +++ b/src/backend/models.ts @@ -84,6 +84,7 @@ export function useBackendModels() { const [models, setModels] = useState([]); const [settings, setSettings] = useState(null); const [error, setError] = useState(""); + const [loading, setLoading] = useState(true); const [connected, setConnected] = useState(false); const refresh = useCallback(async () => { try { @@ -98,6 +99,8 @@ export function useBackendModels() { } catch (failure) { setConnected(false); setError(errorMessage(failure)); + } finally { + setLoading(false); } }, []); useEffect(() => { @@ -155,6 +158,7 @@ export function useBackendModels() { settings, error, connected, + loading, refresh, save, saveMany, diff --git a/src/canvas/CanvasWorkspace.tsx b/src/canvas/CanvasWorkspace.tsx index 9df0e934..b0f2aa96 100644 --- a/src/canvas/CanvasWorkspace.tsx +++ b/src/canvas/CanvasWorkspace.tsx @@ -259,6 +259,8 @@ import { ProjectNameDialog } from "./ProjectNameDialog"; import { ProjectSelector } from "./ProjectSelector"; import { referencePresentation, referenceSnapshot } from "./referenceEvidence"; import "./settings.css"; +import { modelSetupMissing, dismissModelSetup, shouldShowModelSetup } from "./ModelSetup"; +import { WelcomeScreen } from "./WelcomeScreen"; import { SettingsDialog } from "./SettingsDialog"; import { alignNodes, type Alignment } from "./snapping"; import { StackEditor } from "./StackEditor"; @@ -675,6 +677,19 @@ function Workspace() { const [settingsCategory, setSettingsCategory] = useState< "general" | "models" >("general"); + const [welcomeOpen, setWelcomeOpen] = useState(false); + const setupChecked = useRef(false); + useEffect(() => { + if (setupChecked.current || !backendCatalog.connected) return; + setupChecked.current = true; + const preview = new URLSearchParams(window.location.search).get("setup"); + if (preview === "models") { + setSettingsCategory("models"); + setSettingsOpen(true); + } else if (preview === "welcome" || (shouldShowModelSetup() && modelSetupMissing(backendCatalog.models).length > 0)) { + setWelcomeOpen(true); + } + }, [backendCatalog.connected, backendCatalog.models]); const theme = useTheme(); const serviceRuns = useRef(new Map()); const [activeGenerations, setActiveGenerations] = useState< @@ -6565,12 +6580,16 @@ function Workspace() { onClose={closeEditor} /> )} + {welcomeOpen && { dismissModelSetup(); setWelcomeOpen(false); }} + />} {settingsOpen && ( setSettingsOpen(false)} + onClose={() => { dismissModelSetup(); setSettingsOpen(false); }} /> )} {help && ( diff --git a/src/canvas/ModelSettings.tsx b/src/canvas/ModelSettings.tsx index 71adb971..060bd2de 100644 --- a/src/canvas/ModelSettings.tsx +++ b/src/canvas/ModelSettings.tsx @@ -1,3 +1,4 @@ +import { ModelSetup, modelSetupMissing, type SetupKind } from "./ModelSetup"; import { ModelLogo } from "./ModelLogo"; import { providerLabel } from "./providerPresets"; import { useState } from "react"; @@ -9,9 +10,9 @@ import { ModelServiceForm, type SettingsModelKind } from "./ModelServiceForm"; import { CanvasIcon } from "./CanvasIcon"; import "./model-settings.css"; -export function ModelSettings({ catalog }: { catalog: BackendModels }) { +export function ModelSettings({ catalog, onClose, guided = false }: { catalog: BackendModels; onClose: () => void; guided?: boolean }) { const t = useT(); - const [category, setCategory] = useState("agent"); + const [category, setCategory] = useState(null); const [editing, setEditing] = useState<{ kind: SettingsModelKind; model?: ModelConfig; @@ -43,8 +44,28 @@ export function ModelSettings({ catalog }: { catalog: BackendModels }) { models.findIndex((item) => item.providerId === model.providerId) === index, ); + const [skipped, setSkipped] = useState([]); + const missing = modelSetupMissing(catalog.models); + const pending = missing.filter(kind => !skipped.includes(kind)); + function skipItem(kind: SetupKind) { + setSkipped(current => current.includes(kind) ? current : [...current, kind]); + const next = pending.find(item => item !== kind); + if (!next) { onClose(); return; } + setEditing(null); + setCategory(next ?? null); + setDeleting(null); + setError(""); + } + const currentMissing = category !== null && missing.includes(category); + if (category === null) return
+ +
; return (
+ {guided &&
+ {t("modelSetup.count", { count: 4 - missing.length, total: 4 })} +

{t(editing ? "modelSetup.guideSave" : currentMissing ? "modelSetup.guideAdd" : "modelSetup.guideNext")}

+
} {!catalog.connected && (
{catalog.error || t("models.readFailed")} @@ -71,6 +92,7 @@ export function ModelSettings({ catalog }: { catalog: BackendModels }) { className="cw-model-categories" aria-label={t("models.categories")} > + {MODEL_KINDS.map((item) => ( } + + }
); } diff --git a/src/canvas/ModelSetup.tsx b/src/canvas/ModelSetup.tsx new file mode 100644 index 00000000..6232b0dd --- /dev/null +++ b/src/canvas/ModelSetup.tsx @@ -0,0 +1,67 @@ +import type { BackendModels } from "../backend/models"; +import { isGenerationModel, type ModelConfig } from "../backend/rules"; +import { useI18n } from "../i18n/I18nProvider"; +import { CanvasIcon } from "./CanvasIcon"; +import "./model-setup.css"; + +const kinds = ["agent", "image", "video", "audio"] as const; +export type SetupKind = typeof kinds[number]; +const preference = "opendirector.modelSetup.v1"; +let dismissed = false; +export function dismissModelSetup() { + dismissed = true; + try { localStorage.setItem(preference, "dismissed"); } catch { /* Session fallback. */ } +} +export function shouldShowModelSetup() { + if (dismissed) return false; + try { return !localStorage.getItem(preference); } catch { return true; } +} +export function modelSetupMissing(models: ModelConfig[]) { + return kinds.filter(kind => !models.some(model => model.kind === kind && isGenerationModel(model))); +} + +export function ModelSetup({ catalog, onClose, onConfigure, skipped = [], onSkipItem }: { + skipped?: SetupKind[]; + onSkipItem?: (kind: SetupKind) => void; + catalog: BackendModels; + onClose: () => void; + onConfigure: (kind: SetupKind) => void; +}) { + const { t, locale, setLocale } = useI18n(); + const missing = modelSetupMissing(catalog.models); + const next = missing.find(kind => !skipped.includes(kind)); + return
+
+ + +
+
+

{t("modelSetup.hint")}

+
+ {catalog.loading ?

{t("modelSetup.loading")}

: !catalog.connected ? +
{catalog.error || t("modelSetup.failed")} + +
: <> +

{t("modelSetup.count", { count: kinds.length - missing.length, total: kinds.length })}

+
{kinds.map(kind => { + const configured = !missing.includes(kind); + return
+ +
{t(`modelSetup.${kind}`)}

{t(`modelSetup.${kind}Hint`)}

+ {configured && }{t(configured ? "modelSetup.configured" : skipped.includes(kind) ? "modelSetup.deferred" : "modelSetup.missing")} + +
; + })}
+ + } +
+

{t("modelSetup.external")}

+
{next && onSkipItem && } +
+
+
; +} diff --git a/src/canvas/SettingsDialog.tsx b/src/canvas/SettingsDialog.tsx index d0e75dc0..45a89f57 100644 --- a/src/canvas/SettingsDialog.tsx +++ b/src/canvas/SettingsDialog.tsx @@ -8,8 +8,8 @@ import { useI18n, useT } from "../i18n/I18nProvider"; import type { Locale } from "../i18n/types"; const categories = [ - { id: "general", labelKey: "settings.general", icon: "settings" }, { id: "models", labelKey: "settings.models", icon: "model" }, + { id: "general", labelKey: "settings.general", icon: "settings" }, ] as const; function GeneralSettings({ @@ -230,7 +230,7 @@ export function SettingsDialog({ {category === "general" ? ( ) : ( - + )}
diff --git a/src/canvas/WelcomeScreen.tsx b/src/canvas/WelcomeScreen.tsx new file mode 100644 index 00000000..11a6a992 --- /dev/null +++ b/src/canvas/WelcomeScreen.tsx @@ -0,0 +1,84 @@ +import { useLayoutEffect, useRef, useState } from "react"; +import type { BackendModels } from "../backend/models"; +import { ModelSettings } from "./ModelSettings"; +import "./settings.css"; +import { useI18n } from "../i18n/I18nProvider"; +import animatedLogo from "../assets/open_director_logo_anim.gif"; +import staticLogo from "../assets/open_director_logo.png"; +import "./welcome-screen.css"; + +export function WelcomeScreen({ catalog, onSkip }: { catalog: BackendModels; onSkip: () => void }) { + const { t, locale, setLocale } = useI18n(); + const dialog = useRef(null); + const [configuring, setConfiguring] = useState(false); + const [completed, setCompleted] = useState(false); + function finishSetup() { setCompleted(true); } + const previous = useRef([]); + const panel = useRef(null); + const transitioning = useRef(false); + async function beginSetup() { + if (transitioning.current) return; + transitioning.current = true; + const actions = dialog.current?.querySelector(".cw-welcome-intro-actions"); + if (actions && !window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + await actions.animate([{ opacity: 1 }, { opacity: 0, transform: "translateY(8px)" }], { duration: 160, fill: "forwards" }).finished; + } + previous.current = Array.from(dialog.current!.querySelectorAll("[data-welcome-shared]")).map(element => element.getBoundingClientRect()); + setConfiguring(true); + } + useLayoutEffect(() => { + if (!configuring) return; + if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + dialog.current!.querySelectorAll("[data-welcome-shared]").forEach((element, index) => { + const before = previous.current[index]; + const after = element.getBoundingClientRect(); + if (!before || !after.width) return; + element.animate([ + { transformOrigin: "top left", transform: `translate(${before.left - after.left}px, ${before.top - after.top}px) scale(${before.width / after.width}, ${before.height / after.height})` }, + { transformOrigin: "top left", transform: "none" }, + ], { duration: 760, easing: "cubic-bezier(0.22, 1, 0.36, 1)" }); + }); + } + panel.current?.focus({ preventScroll: true }); + }, [configuring]); + useLayoutEffect(() => { + const element = dialog.current!; + element.showModal(); + return () => element.close(); + }, []); + return { event.preventDefault(); if (configuring && !completed) finishSetup(); else onSkip(); }}> +
+ + +
+
+
+ +

{t("modelSetup.welcomeTitle")}

+

{t("modelSetup.welcomeDescription")}

+
+ {!configuring &&
+
+ + +
+

{t("modelSetup.welcomeNote")}

+
} + {configuring && !completed &&
+
+
} + {completed &&
+ +

{t("modelSetup.finishedTitle")}

+

{t("modelSetup.finishedHint")}

+ +
} +
+
{configuring && !completed && }OpenDirector
+
; +} diff --git a/src/canvas/model-setup.css b/src/canvas/model-setup.css new file mode 100644 index 00000000..18cbfe58 --- /dev/null +++ b/src/canvas/model-setup.css @@ -0,0 +1,28 @@ +.cw-model-setup { width: 100%; display: flex; flex-direction: column; position: relative; } +.cw-model-setup-language { display: flex; justify-content: flex-end; align-items: center; gap: 8px; color: var(--settings-muted); font-size: 12px; } +.cw-model-setup-language select { width: auto; min-height: 30px; } +.cw-model-setup-intro { padding: 12px 0 16px; } +.cw-model-setup-intro p { margin: 0; color: var(--settings-muted); line-height: 1.6; font-size: 13px; } +.cw-model-setup-progress { font-size: 12px; color: var(--settings-muted); margin: 0 0 12px; } +.cw-model-setup-list { overflow: hidden; border: 1px solid var(--settings-border); border-radius: 12px; background: var(--settings-surface); } +.cw-model-setup-row { display: flex; gap: 12px; align-items: center; padding: 15px 16px; } +.cw-model-setup-row + .cw-model-setup-row { border-top: 1px solid var(--settings-border); } +.cw-model-setup-icon { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 8px; background: var(--settings-hover); color: var(--settings-muted); flex-shrink: 0; } +.cw-model-setup-copy { flex: 1; min-width: 0; } +.cw-model-setup-copy strong { font-size: 14px; font-weight: 500; } +.cw-model-setup-copy p { color: var(--settings-muted); margin: 4px 0 0; font-size: 12px; line-height: 1.5; } +.cw-model-setup-status { font-size: 12px; color: var(--settings-muted); flex-shrink: 0; } +.cw-model-setup-status.is-configured { color: #75d69a; } +:root[data-theme="light"] .cw-model-setup-status.is-configured { color: #237544; } +.cw-model-setup-footer { display: flex; align-items: flex-end; gap: 16px; padding-top: 20px; } +.cw-model-setup-footer > p { flex: 1; font-size: 12px; color: var(--settings-muted); line-height: 1.7; margin: 0; } +.cw-model-setup-footer > div { display: flex; align-items: center; gap: 8px; flex-shrink: 0; } +.cw-settings-body:has(.cw-model-setup) { overflow-y: auto; } +.cw-settings-body:has(.cw-model-setup) > .cw-model-settings { flex-shrink: 0; } +@media (max-width: 600px) { + .cw-model-setup-row { flex-wrap: wrap; gap: 10px; padding: 12px; } + .cw-model-setup-copy { flex-basis: calc(100% - 50px); } + .cw-model-setup-status { margin-left: 44px; margin-right: auto; } + .cw-model-setup-footer { flex-direction: column; align-items: stretch; } + .cw-model-setup-footer > div { justify-content: flex-end; flex-wrap: wrap; } +} diff --git a/src/canvas/welcome-screen.css b/src/canvas/welcome-screen.css new file mode 100644 index 00000000..475bbb8f --- /dev/null +++ b/src/canvas/welcome-screen.css @@ -0,0 +1,73 @@ +.cw-welcome { position: fixed; inset: 0; box-sizing: border-box; width: 100vw; height: 100dvh; max-width: none; max-height: none; margin: 0; padding: 0; border: 0; background: var(--settings-background); color: var(--settings-text); font: 14px Inter, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; overflow-y: auto; } +.cw-welcome::backdrop { background: var(--settings-background); } +.cw-welcome-language { position: absolute; top: 28px; right: 36px; display: flex; align-items: center; gap: 12px; color: var(--settings-muted); font-size: 12px; } +.cw-welcome-language select { color: var(--settings-text); background: var(--settings-input); border: 1px solid var(--settings-border); border-radius: 8px; padding: 8px 12px; font: inherit; } +.cw-welcome-content { box-sizing: border-box; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; min-height: 100%; padding: 100px 24px 110px; } +.cw-welcome-logo { width: 144px; height: 144px; margin-bottom: 32px; } +.cw-welcome-logo img { width: 100%; height: 100%; object-fit: contain; } +.cw-welcome-static { display: none; } +.cw-welcome h1 { margin: 0; font-size: clamp(30px, 4vw, 48px); line-height: 1.2; letter-spacing: -.035em; font-weight: 600; outline: none; } +.cw-welcome-description { margin: 20px 0 0; color: var(--settings-muted); font-size: 16px; line-height: 1.8; max-width: 480px; } +.cw-welcome-actions { display: flex; flex-direction: column; gap: 10px; margin-top: 36px; width: min(260px, 100%); } +.cw-welcome-actions button { cursor: pointer; min-height: 46px; padding: 12px 24px; font: inherit; border-radius: 10px; } +.cw-welcome-primary { border: 1px solid var(--cw-control-accent); background: var(--cw-control-accent); color: var(--cw-control-on-accent); font-weight: 600 !important; } +.cw-welcome-primary:hover { background: var(--cw-control-accent-hover); } +.cw-welcome-secondary { border: 1px solid transparent; background: transparent; color: var(--settings-muted); } +.cw-welcome-secondary:hover { color: var(--settings-text); background: var(--settings-hover); } +.cw-welcome :is(button, select):focus-visible { outline: 2px solid var(--cw-control-accent); outline-offset: 4px; } +.cw-welcome-note { margin: 16px 0 0; font-size: 12px; line-height: 1.7; color: var(--settings-muted); max-width: 400px; } +.cw-welcome-footer { position: absolute; bottom: 28px; left: 0; width: 100%; text-align: center; color: var(--settings-muted); font-size: 12px; } +@media (prefers-reduced-motion: reduce) { .cw-welcome-animation { display: none; } .cw-welcome-static { display: block; } } +@media (max-width: 600px) { .cw-welcome-language { top: 20px; right: 20px; } .cw-welcome-logo { width: 112px; height: 112px; } } + +.cw-welcome-hero { display: flex; flex-direction: column; align-items: center; flex-shrink: 0; } +.cw-welcome-intro-actions { display: flex; flex-direction: column; align-items: center; width: 100%; } +.cw-welcome.is-configuring .cw-welcome-content { justify-content: flex-start; padding: 54px 24px 72px; } +.cw-welcome.is-configuring .cw-welcome-logo { width: 56px; height: 56px; margin-bottom: 14px; } +.cw-welcome.is-configuring h1 { font-size: 28px; letter-spacing: -.025em; } +.cw-welcome.is-configuring .cw-welcome-description { font-size: 14px; margin-top: 10px; } +.cw-welcome .cw-welcome-setup { position: relative; box-sizing: border-box; width: min(680px, 100%); height: auto; max-height: none; margin: 28px auto 0; padding: 0; overflow: visible; border: 0; border-radius: 0; background: transparent; box-shadow: none; text-align: left; outline: none; animation: welcome-setup-reveal 650ms 180ms cubic-bezier(.22,1,.36,1) both; } +.cw-welcome-setup .cw-settings-body { padding: 0; overflow: visible; } +.cw-welcome-setup .cw-model-settings { overflow: visible; } +.cw-welcome-setup .cw-model-setup-language { display: none; } +.cw-welcome-setup .cw-model-setup-intro { padding-top: 0; } +.cw-welcome.is-configuring .cw-welcome-footer { position: static; margin-top: -40px; padding-bottom: 20px; } +@keyframes welcome-setup-reveal { from { opacity: 0; transform: translateY(28px); } to { opacity: 1; transform: translateY(0); } } +@media (max-width: 600px) { .cw-welcome.is-configuring .cw-welcome-content { padding-top: 90px; } .cw-welcome.is-configuring h1 { font-size: 24px; } } +@media (prefers-reduced-motion: reduce) { .cw-welcome .cw-welcome-setup { animation: none; } } + +/* The full-screen setup shares one continuous page surface. */ +.cw-welcome-setup .cw-model-editor-footer { + background: transparent; + border-top: 0; + padding: 20px 0 0; +} + +.cw-model-guide-progress { margin-bottom: 20px; color: var(--settings-muted); font-size: 12px; } +.cw-model-guide-progress p { margin: 6px 0 0; line-height: 1.6; } +.cw-model-guide-footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 24px; padding-bottom: 4px; } +.cw-welcome-footer { display: flex; flex-direction: column; align-items: center; gap: 16px; } +.cw-welcome.is-configuring .cw-welcome-footer { margin-top: 0; } +.cw-welcome-skip-all { border: 0; background: transparent; padding: 8px 14px; color: var(--settings-muted); cursor: pointer; font: inherit; border-radius: 8px; } +.cw-welcome-skip-all:hover { color: var(--settings-text); background: var(--settings-hover); } + +/* Keep the global exit visible independently of setup content height. */ +.cw-welcome.is-configuring .cw-welcome-content { padding-bottom: 120px; } +.cw-welcome.is-configuring .cw-welcome-footer { + position: fixed; + inset: auto 0 0; + width: 100%; + box-sizing: border-box; + margin: 0; + padding: 12px 24px 20px; + gap: 8px; + background: var(--settings-background); + z-index: 2; +} +.cw-welcome-complete { margin: 64px auto 0; max-width: 420px; text-align: center; animation: welcome-setup-reveal 450ms ease-out both; } +.cw-welcome-complete-check { display: grid; place-items: center; width: 52px; height: 52px; margin: 0 auto 22px; border-radius: 50%; color: #75d69a; background: #75d69a15; font-size: 26px; } +:root[data-theme="light"] .cw-welcome-complete-check { color: #237544; background: #23754415; } +.cw-welcome-complete h2 { margin: 0; font-size: 24px; font-weight: 600; } +.cw-welcome-complete p { margin: 14px 0 28px; color: var(--settings-muted); font-size: 14px; line-height: 1.8; } +.cw-welcome-complete button { min-width: 180px; min-height: 44px; border-radius: 10px; padding: 10px 24px; font: inherit; cursor: pointer; } +@media (prefers-reduced-motion: reduce) { .cw-welcome-complete { animation: none; } } diff --git a/src/i18n/messages/en-US/index.ts b/src/i18n/messages/en-US/index.ts index 62aa1cc4..2a759310 100644 --- a/src/i18n/messages/en-US/index.ts +++ b/src/i18n/messages/en-US/index.ts @@ -1,3 +1,4 @@ +import { modelSetup } from "./modelSetup.ts"; import type { MessageEntry } from "../../types.ts"; import { api } from "./api.ts"; import { assistant } from "./assistant.ts"; @@ -21,6 +22,7 @@ type ZhCatalog = typeof import("../zh-CN/index.ts").zhCN; export const enUS: Record = { ...common, ...settings, + ...modelSetup, ...canvas, ...canvasShell, ...canvasNodes, diff --git a/src/i18n/messages/en-US/modelSetup.ts b/src/i18n/messages/en-US/modelSetup.ts new file mode 100644 index 00000000..32439ad6 --- /dev/null +++ b/src/i18n/messages/en-US/modelSetup.ts @@ -0,0 +1,53 @@ +export const modelSetup = { + "modelSetup.nextModel": "Next: configure {model}", + "modelSetup.finishedTitle": "Setup complete", + "modelSetup.finishedHint": "Your saved model configurations are kept. You can add other models anytime in Settings.", + + "modelSetup.skipItem": "Skip this type", + "modelSetup.skipAll": "Skip all setup", + "modelSetup.deferred": "Set up later", + + "modelSetup.guideSave": "Enter connection details, select models, then save the service to continue.", + "modelSetup.guideAdd": "Add a service to configure this model type.", + "modelSetup.guideNext": "This model type is configured. Continue with another type.", + + "modelSetup.welcomeTitle": "Welcome to OpenDirector", + "modelSetup.welcomeDescription": "Start with an idea. Create your video together with AI.", + "modelSetup.welcomeConfigure": "Set up creative models", + "modelSetup.welcomeSkip": "Explore first", + "modelSetup.welcomeNote": "Connect your own model services, or set them up later in Settings.", + + "modelSetup.done": "Done", + "modelSetup.overview": "Overview", + "modelSetup.title": "Set up creative models", + "modelSetup.intro": "Connect models to use the AI assistant and generate images, videos, or audio.", + "modelSetup.hint": "Set up what you need now. Add other types later.", + "modelSetup.agent": "Text model", + "modelSetup.image": "Image model", + "modelSetup.audio": "Audio model", + "modelSetup.audioHint": "Generate speech and music for video narration and soundtracks", + "modelSetup.audioAction": "Configure audio model", + "modelSetup.video": "Video model", + "modelSetup.agentHint": "Help the AI assistant understand requests and plan creative tasks", + "modelSetup.imageHint": "Generate and edit images", + "modelSetup.videoHint": "Generate videos from text or reference media", + "modelSetup.configured": "Configured", + "modelSetup.missing": "Not configured", + "modelSetup.configure": "Configure", + "modelSetup.manage": "Manage", + "modelSetup.skip": "Skip for now", + "modelSetup.enter": "Enter workspace", + "modelSetup.back": "Back to setup", + "modelSetup.later": "Continue anytime in Settings → Model connections.", + "modelSetup.complete": "Models configured", + "modelSetup.count": "{count} of {total} model types configured", + "modelSetup.agentAction": "Configure text model", + "modelSetup.imageAction": "Configure image model", + "modelSetup.videoAction": "Configure video model", + "modelSetup.external": "External agents can also use their own model configuration. Check runtime status in the assistant.", + "modelSetup.loading": "Loading model configuration…", + "modelSetup.failed": "Could not read model configuration. Try again.", + "modelSetup.retry": "Reload", + "modelSetup.language": "Interface language", + "modelSetup.nav": "Model setup" +} as const; diff --git a/src/i18n/messages/zh-CN/index.ts b/src/i18n/messages/zh-CN/index.ts index c0105d23..6fe7538a 100644 --- a/src/i18n/messages/zh-CN/index.ts +++ b/src/i18n/messages/zh-CN/index.ts @@ -1,3 +1,4 @@ +import { modelSetup } from "./modelSetup.ts"; import { api } from "./api.ts"; import { assistant } from "./assistant.ts"; import { canvas } from "./canvas.ts"; @@ -18,6 +19,7 @@ import { tools } from "./tools.ts"; export const zhCN = { ...common, ...settings, + ...modelSetup, ...canvas, ...canvasShell, ...canvasNodes, diff --git a/src/i18n/messages/zh-CN/modelSetup.ts b/src/i18n/messages/zh-CN/modelSetup.ts new file mode 100644 index 00000000..a51b34dd --- /dev/null +++ b/src/i18n/messages/zh-CN/modelSetup.ts @@ -0,0 +1,53 @@ +export const modelSetup = { + "modelSetup.nextModel": "下一步:配置{model}", + "modelSetup.finishedTitle": "配置已完成", + "modelSetup.finishedHint": "已保存的模型配置会保留,其余模型可随时在设置中补充。", + + "modelSetup.skipItem": "跳过此项", + "modelSetup.skipAll": "跳过全部配置", + "modelSetup.deferred": "稍后配置", + + "modelSetup.guideSave": "填写服务连接并选择模型,然后点击「保存服务」继续。", + "modelSetup.guideAdd": "添加一个服务,开始配置当前类型的模型。", + "modelSetup.guideNext": "此类模型已配置,可以继续配置其他类型。", + + "modelSetup.welcomeTitle": "欢迎来到 OpenDirector", + "modelSetup.welcomeDescription": "从一个想法开始,与 AI 一起创作你的视频。", + "modelSetup.welcomeConfigure": "配置创作模型", + "modelSetup.welcomeSkip": "先探索一下", + "modelSetup.welcomeNote": "连接你自己的模型服务,也可以稍后在设置中配置。", + + "modelSetup.done": "完成", + "modelSetup.overview": "概览", + "modelSetup.title": "配置创作模型", + "modelSetup.intro": "连接模型后,可使用 AI 助手、生成图片、视频或音频。", + "modelSetup.hint": "先配置需要的类型,稍后随时补充。", + "modelSetup.agent": "文本模型", + "modelSetup.image": "图片模型", + "modelSetup.audio": "音频模型", + "modelSetup.audioHint": "生成语音和音乐,为视频配音、配乐", + "modelSetup.audioAction": "配置音频模型", + "modelSetup.video": "视频模型", + "modelSetup.agentHint": "让 AI 助手理解需求、安排创作任务", + "modelSetup.imageHint": "生成和编辑图片", + "modelSetup.videoHint": "根据文字或参考素材生成视频", + "modelSetup.configured": "已配置", + "modelSetup.missing": "未配置", + "modelSetup.configure": "配置", + "modelSetup.manage": "管理", + "modelSetup.skip": "暂时跳过", + "modelSetup.enter": "进入工程", + "modelSetup.back": "返回配置清单", + "modelSetup.later": "随时从「设置 → 模型接入」继续。", + "modelSetup.complete": "模型已配置", + "modelSetup.count": "已配置 {count} / {total} 类模型", + "modelSetup.agentAction": "配置文本模型", + "modelSetup.imageAction": "配置图片模型", + "modelSetup.videoAction": "配置视频模型", + "modelSetup.external": "外部 Agent 也可使用其自身的模型配置;运行状态请在助手中查看。", + "modelSetup.loading": "正在读取模型配置…", + "modelSetup.failed": "暂时无法读取模型配置,请重试。", + "modelSetup.retry": "重新读取", + "modelSetup.language": "界面语言", + "modelSetup.nav": "模型配置引导" +} as const;