diff --git a/design/inkseq-v2-prototype/HANDOFF.md b/design/inkseq-v2-prototype/HANDOFF.md index 52504a90..c434f2b1 100644 --- a/design/inkseq-v2-prototype/HANDOFF.md +++ b/design/inkseq-v2-prototype/HANDOFF.md @@ -18,6 +18,22 @@ feature in the canonical frontend (`inkseq/inkseq-design`, `studio-app`) on the `src/layouts.jsx` (story workspace, bulk bar), `src/library.jsx`, `src/gpu.jsx`, `src/agent.jsx`, `src/trashPage.jsx`, `src/advanced.jsx` (all H3 controls), `src/inputs.jsx`. +## Launch batch A/B (2026-10-19) — accounts, credits, admin, AI-first onboarding, folders + +Backend specs: `inkseq/h3-studio#303` (epic) → #304 roles, #305 sign-in, #306 credits, #307 Stripe, #308 legal, +#309 "Hand it to AI" import, #310 library folders. Use **Preview as** (bottom-left) to switch User / Admin / Signed out. + +- **Users see credits only** (1 credit = US$0.01). Header chip → Settings → Credits: balance, today, packs, price table + (credits per second by tier), limits in credits, history. Estimates read "≈ N credits · ≈ M min". Not enough credits → + modal, nothing queued. Agent daily caps are in credits. (`src/account.jsx`, `src/model.js` `estimate`). +- **Admins only**: GPUs nav and Settings → Admin (Users with role/status/credit adjustment with reason, Pricing & margin + with live margin per tier, Test machines, Audit log). Regular users never start machines. +- **Sign in** (`?page=auth`): Google / Apple / GitHub / 6-digit email code, implied consent line, welcome credits. + Account → Sign-in methods. +- **New story is AI-first**: "Hand it to AI" (recommended) imports a whole story into a storyboard and opens it; + "Use my own AI" opens the connect sheet; logline / script / blank remain. Works page shows the hint strip. +- **Library opens on folders**: one per story, Quick Create, Uploaded not used yet, Inbox; derived from usage, stored once. + ## Rules for the implementation 1. **Every feature ships for both the browser and Agents.** Browser and Agent use the same business API. diff --git a/design/inkseq-v2-prototype/src/account.jsx b/design/inkseq-v2-prototype/src/account.jsx new file mode 100644 index 00000000..e01f4031 --- /dev/null +++ b/design/inkseq-v2-prototype/src/account.jsx @@ -0,0 +1,247 @@ +import React, {useState} from 'react'; +import {ArrowRight, Check, Coins, CreditCard, Gift, History, Lock, Mail, RotateCcw, ShieldCheck, Sparkles, TrendingUp, UserRound, Users, X} from 'lucide-react'; +import {PACKS, useStore} from './store.jsx'; +import {CREDIT_USD, fmtCredits} from './model.js'; + +// Launch batch A (prototype): sign-in, credits, top-up, admin console. Users only ever see credits; +// dollars, machine cost and margin are admin-only. Every number here comes from the backend in production. + +const ago = t => { const m = Math.round((Date.now() - t) / 60000); return m < 1 ? '刚刚' : m < 60 ? `${m} 分钟前` : m < 1440 ? `${Math.round(m / 60)} 小时前` : `${Math.round(m / 1440)} 天前`; }; +const num = n => Math.round(n).toLocaleString('en-US'); + +/* ---------- 登录 / 注册 ---------- */ +const PROVIDERS = [ + ['google', '用 Google 继续', ], + ['apple', '用 Apple 继续', ], + ['github', '用 GitHub 继续', ], +]; + +export function AuthPage() { + const {dispatch} = useStore(); + const [email, setEmail] = useState(''), [sent, setSent] = useState(false), [code, setCode] = useState(''); + const go = () => dispatch({type: 'signIn'}); + const okEmail = /.+@.+\..+/.test(email); + return ( +
+
+ 映序 +

登录或注册

+

用一个账号做故事、快速创作,也能让你的 AI 助手帮你做。新用户验证邮箱后送 100 积分,够生成 5 条标准视频。

+
{PROVIDERS.map(([k, l, icon]) => + )}
+
或者用邮箱
+ {!sent ? <> + + + : <> +

验证码已发到 {email},10 分钟内有效。

+ + + } + 继续即表示你同意 用户协议、隐私政策 和 内容规则。 + 没有密码要记。同一个邮箱用 Google、Apple 或验证码登录,都是同一个账号。 +
+

原型:任意方式都会直接登录为普通用户。

+
+ ); +} + +/* ---------- 顶栏积分 ---------- */ +export function CreditsChip() { + const {state, dispatch} = useStore(); + const low = state.credits.balance < 100; + return ; +} + +/* ---------- 设置 → 积分 ---------- */ +const KIND = {purchase: ['充值', CreditCard], grant: ['赠送', Gift], spend: ['使用', Sparkles], refund: ['退回', RotateCcw], adjust: ['调整', ShieldCheck]}; +export const TIERS = [['fast', '快速草稿', '最快,细节稍软,适合先看效果'], ['draft', '标准', '480P · 20 步,大多数时候用这个'], ['final', '精修', '768P 完整模型,成片用']]; + +export function CreditsPanel({LimitField}) { + const {state} = useStore(); + const [buy, setBuy] = useState(null); + const c = state.credits, L = state.limits; + const perClip = k => Math.ceil(state.rates[k] * 5); + const pct = L.daily ? Math.min(100, (state.creditsToday || 0) / L.daily * 100) : 0; + return <> +

我的积分

+
+
余额
{num(c.balance)}积分
+ 大约够 {Math.floor(c.balance / perClip('draft'))} 条 5 秒标准视频 · 积分不会过期
+
今天已用{fmtCredits(state.creditsToday || 0)} + {L.daily ? <>每日上限 {fmtCredits(L.daily)} : 不设每日上限}
+
+ +

充值

+
{PACKS.map(p => )}
+ 通过 Stripe 安全支付:银行卡、Apple Pay、Google Pay、支付宝。我们不保存你的卡号。
+ +

生成要多少积分

+
{TIERS.map(([k, l, d]) =>
{l}{d} + {state.rates[k]} 积分 / 秒5 秒一条 ≈ {perClip(k)} 积分
)}
+ 提交前会先告诉你要用多少;生成失败的自动退回,不扣积分。
+ +

用量提醒

+ + + AI 助手各自的每日上限在「AI 助手」里单独设。
+ +

积分记录

+
{[...c.entries].reverse().slice(0, 12).map(e => { const [l, I] = KIND[e.kind] || KIND.adjust; return ( +
{e.label}{l} · {ago(e.at)} + 0 ? 'good' : ''}>{e.amount > 0 ? '+' : '−'}{num(Math.abs(e.amount))}
); })}
+ 需要发票或退款,在每条充值记录里下载收据,或联系 support@inkseq.com。
+ {buy && setBuy(null)} />} + ; +} + +// Stripe Checkout is a hosted page; the prototype only shows the hand-off and the result. +function Checkout({pack, onClose}) { + const {dispatch, toast} = useStore(); + const [step, setStep] = useState('confirm'); + return
e.stopPropagation()}> +
充值 {pack.name}包
+ {step === 'confirm' ? <> +
{num(pack.credits)} 积分{pack.bonus ? `(含多送 ${pack.bonus}%)` : ''}US${pack.usd}.00
+ 接下来会打开 Stripe 的安全付款页面。付款成功后积分马上到账,这个页面会自动刷新。税费按你所在地区在付款页显示。 +
+
+ : step === 'stripe' ?
+ checkout.stripe.comInkseq · {num(pack.credits)} creditsUS${pack.usd}.00 +
4242 4242 4242 4242 · 12/34 · 123
+ +
: <> +
已到账 {num(pack.credits)} 积分,收据已发到你的邮箱。
+
+ } +
; +} + +// Shown when a submit needs more credits than the balance; nothing is queued. +export function LowCredits() { + const {state, dispatch} = useStore(); + const b = state.creditBlock; + if (!b) return null; + return
dispatch({type: 'creditBlockClose'})}>
e.stopPropagation()}> +
积分不够
+

这次要用 {fmtCredits(b.need)},你还有 {fmtCredits(b.have)}。没有提交,也没有扣积分。

+

可以少生成几条、换成快速草稿,或者先充值。

+
+
+
; +} + +/* ---------- 设置 → 账号:登录方式 ---------- */ +export function LinkedLogins() { + const {state, toast} = useStore(); + const admin = state.role === 'admin'; + const rows = [['邮箱', admin ? 'dan@inkseq.com' : 'mia.c@gmail.com', true], ['Google', admin ? '' : 'mia.c@gmail.com', !admin], ['Apple', '', false], ['GitHub', '', false]]; + return

登录方式

+ {rows.map(([k, v, on]) =>
{k}{on ? v || '已连接' : '未连接'}
+ {on ? : }
)} + 至少保留一种登录方式。邮箱用来收验证码和收据。
; +} + +/* ---------- 设置 → 管理员 ---------- */ +// Measured GPU seconds per 5 s clip (2026-10-15 benchmark) and the hourly price of the class it runs on. +const COST_BASIS = {fast: {sec: 29, hourly: 0.64, gpu: 'RTX 5090'}, draft: {sec: 87, hourly: 0.64, gpu: 'RTX 5090'}, final: {sec: 217, hourly: 1.29, gpu: 'PRO 6000'}}; +const FEE = clip => clip * 0.035; // card processing, roughly + +export function AdminPanel({testMachines}) { + const [tab, setTab] = useState('users'); + return <> +

管理员

+

只有管理员能看到这一页。普通用户只看到积分,看不到美元、机器和利润。

+
{[['users', '用户', Users], ['pricing', '定价与利润', TrendingUp], ['machines', '测试机', ShieldCheck], ['log', '操作记录', History]].map(([k, l, I]) => + )}
+ {tab === 'users' && } + {tab === 'pricing' && } + {tab === 'machines' && testMachines} + {tab === 'log' && } + ; +} + +function AdminUsers() { + const {state, dispatch} = useStore(); + const [q, setQ] = useState(''), [open, setOpen] = useState(null), [delta, setDelta] = useState(100), [reason, setReason] = useState(''); + const list = state.users.filter(u => !q || (u.name + u.email).toLowerCase().includes(q.toLowerCase())); + return
+
setQ(e.target.value)} />
{state.users.length} 个账号
+
+
账号角色余额30 天用量
+ {list.map(u => +
+ {u.name}{u.status === 'suspended' && · 已停用}{u.email} · {u.via.join(' / ')} · {ago(u.joined)}加入 + + {num(u.balance)}{num(u.spent30)} + +
+ {open === u.id &&
+ + + + +
} +
)} +
+
; +} + +function AdminPricing() { + const {state, dispatch} = useStore(); + const k = state.overhead; + const rows = Object.entries(COST_BASIS).map(([tier, b]) => { + const price = Math.ceil(state.rates[tier] * 5) * CREDIT_USD, gpu = b.sec / 3600 * b.hourly, cost = gpu * k + FEE(price); + return {tier, b, price, gpu, cost, margin: price ? 1 - cost / price : 0}; + }); + const label = Object.fromEntries(TIERS.map(([t, l]) => [t, l])); + return <> +

每档价格(积分 / 秒视频)

+ 1 积分 = US$0.01。改完先看下面的利润,再点「发布」;已经提交的任务按提交时的价格算。当前 v{state.ratesRev}。 +
{TIERS.map(([t, l]) => )} +
+

每条 5 秒视频的利润

+ +
+
档位用户付纯机器算上放大和手续费毛利
+ {rows.map(r =>
{label[r.tier]}{r.b.gpu} · {r.b.sec} 秒 · ${r.b.hourly}/时 + ${r.price.toFixed(2)}${r.gpu.toFixed(3)}${r.cost.toFixed(3)} + {Math.round(r.margin * 100)}%
)} +
+ 毛利低于 30% 标红。参考:MiniMax 官方 H3 约 $0.40–0.65 / 5 秒,Kling 约 $0.40–0.55,Runway 约 $0.25–0.60。
+

最近 30 天

+
用户用掉{num(41200)} 积分= $412.00
+
实际机器费用$96.40来自供应商账单
+
毛利77%另有 $3.04 免除、$0.80 待对账
+ ; +} + +function AdminLog() { + const {state} = useStore(); + return

管理员操作记录

+
{[...state.adminLog].reverse().map((l, i) =>
{ago(l.at)} {l.text}
)} + {!state.adminLog.length && 还没有操作。调整积分、改角色、发布定价都会记在这里,不能删除。}
; +} + +/* ---------- 原型:切换预览身份 ---------- */ +export function RolePreview() { + const {state, dispatch} = useStore(); + return
+ 预览身份 +
; +} + +// "新手引导" strip on the home page: recommends the built-in assistant first. +export function AssistantHint({onStart}) { + return ; +} diff --git a/design/inkseq-v2-prototype/src/agent.jsx b/design/inkseq-v2-prototype/src/agent.jsx index fab3e681..29617e07 100644 --- a/design/inkseq-v2-prototype/src/agent.jsx +++ b/design/inkseq-v2-prototype/src/agent.jsx @@ -1,14 +1,16 @@ import React, {useEffect, useState} from 'react'; import {Bot, Check, Copy, ExternalLink, Pause, Play, X} from 'lucide-react'; import {useStore} from './store.jsx'; +import {fmtCredits} from './model.js'; // 让 AI 助手来做:范围 → 能做什么 → 复制一段话给它。连上以后它做的东西会带 🤖 标记,花费有每日上限。 export const SKILL_URL = 'https://inkseq.com/for-agents/SKILL.md'; const PERMS = { - view: {label: '只看和整理', hint: '读作品、写提示词、整理素材和档案;不生成,不花钱'}, - generate: {label: '创作并生成', hint: '还可以提交生成,花费不超过下面的每日上限'}, + view: {label: '只看和整理', hint: '读作品、写提示词、整理素材和档案;不生成,不花积分'}, + generate: {label: '创作并生成', hint: '还可以提交生成,用的积分不超过下面的每日上限'}, }; -const money = n => `$${n.toFixed(2)}`; +// Caps are stored as their US$ price; users see credits (1 credit = US$0.01). +const money = n => fmtCredits(n * 100); export function scopeOptions(state, here) { const opts = []; @@ -30,7 +32,7 @@ export function AgentSheet({here, onClose}) { const text = inv && `请用映序(Inkseq)帮我创作视频。 1. 先读说明:${SKILL_URL} 2. 用这个一次性连接码连接我的账号:${inv.code}(${Math.ceil(left / 60)} 分钟内有效) -3. 范围:${inv.scope.label};权限:${PERMS[inv.perm].label}${inv.perm === 'generate' ? `,每天最多花 ${money(inv.cap)}` : ''}。 +3. 范围:${inv.scope.label};权限:${PERMS[inv.perm].label}${inv.perm === 'generate' ? `,每天最多用 ${money(inv.cap)}` : ''}。 4. 做出来的东西留在映序里,做完把链接发给我,我在网页上看结果、挑版本。`; const make = () => { setCopied(false); dispatch({type: 'agentInvite', scope: scopes[scope], perm, cap}); }; const copy = () => { try { navigator.clipboard?.writeText(text); } catch {} setCopied(true); }; @@ -44,7 +46,7 @@ export function AgentSheet({here, onClose}) { {scopes.map((s, i) => )}
2 · 能做什么 {Object.entries(PERMS).map(([k, p]) => )} - {perm === 'generate' &&
{joined &&
{joined.name} 已连接,可以在「设置 → AI 助手」里查看它做了什么。
} @@ -77,7 +79,7 @@ export function AgentSettings() {
{a.name}· {a.device} · 连接于 {ago(a.connectedAt)}
{a.paused ? 已暂停 : 可用}
范围:{a.scope.label} · {PERMS[a.perm].label} - {a.perm === 'generate' &&
今天已花 {money(a.spent)} / dispatch({type: 'agentCap', id: a.id, cap: +e.target.value})}> {[...new Set([1, 2, 5, 10, 20, 50, a.cap])].sort((x, y) => x - y).map(n => )} 每日上限
}
{a.log.slice(-4).reverse().map((l, i) =>
{ago(l.at)} {l.text}
)} {!a.log.length && 还没做任何事。}
diff --git a/design/inkseq-v2-prototype/src/en.json b/design/inkseq-v2-prototype/src/en.json index 1c125157..44bd799d 100644 --- a/design/inkseq-v2-prototype/src/en.json +++ b/design/inkseq-v2-prototype/src/en.json @@ -1549,5 +1549,203 @@ "AI 匹配暂时不可用,可以手动选造型和照片": "AI matching is unavailable right now — pick looks and photos by hand", "连接于 {}": "connected {1}", "删掉的东西在这里保留 {} 天,随时可以恢复到原来的位置。": "Deleted items stay here for {1} days and can be restored to where they were.", -"删除的故事、镜头、素材和创作会先放在这里。": "Deleted stories, shots, assets and creations land here first." -} \ No newline at end of file +"删除的故事、镜头、素材和创作会先放在这里。": "Deleted stories, shots, assets and creations land here first.", +"积分": "credits", +"{} 积分": "{} credits", +"约 {} 积分 · 约 {} 分钟": "≈ {} credits · ≈ {} min", +"积分余额 · 点击充值": "Credit balance · click to top up", +"余额": "Balance", +"大约够": "Enough for about", +"条 5 秒标准视频 · 积分不会过期": "5-second Standard videos · credits never expire", +"今天已用": "Used today", +"每日上限 {}": "Daily limit {}", +"不设每日上限": "No daily limit", +"充值": "Top up", +"最常选": "Most popular", +"{}包": "{} pack", +"入门包": "Starter pack", +"创作者包": "Creator pack", +"工作室包": "Studio pack", +"专业包": "Pro pack", +"入门": "Starter", +"创作者": "Creator", +"工作室": "Studio", +"专业": "Pro", +"多送 {}%": "{}% bonus", +"通过 Stripe 安全支付:银行卡、Apple Pay、Google Pay、支付宝。我们不保存你的卡号。": "Secure payment through Stripe: cards, Apple Pay, Google Pay, Alipay. We never store your card number.", +"生成要多少积分": "What generation costs", +"快速草稿": "Fast draft", +"最快,细节稍软,适合先看效果": "Fastest, slightly softer detail — good for a first look", +"标准": "Standard", +"480P · 20 步,大多数时候用这个": "480P · 20 steps — use this most of the time", +"768P 完整模型,成片用": "768P full model — for the final cut", +"{} 积分 / 秒": "{} credits / sec", +"5 秒一条 ≈ {} 积分": "5-second clip ≈ {} credits", +"提交前会先告诉你要用多少;生成失败的自动退回,不扣积分。": "You see the cost before you submit; failed generations are refunded automatically.", +"用量提醒": "Usage limits", +"每天最多用": "Daily maximum", +"所有生成和 AI 助手加起来,按悉尼时间每天清零;0 = 不限": "All generation and AI assistants combined, resets daily (Sydney time); 0 = no limit", +"AI 助手各自的每日上限在「AI 助手」里单独设。": "Each AI assistant has its own daily limit under “AI assistants”.", +"积分记录": "Credit history", +"充值 · {}包": "Top-up · {} pack", +"充值 · 创作者包": "Top-up · Creator pack", +"新用户赠送(邮箱已验证)": "Welcome credits (email verified)", +"生成 · 最后一班来信(12 条)": "Generation · Last Train Letter (12 takes)", +"退回 · 生成失败的 1 条": "Refund · 1 failed take", +"生成 · 快速创作「雨夜站台」": "Generation · Quick Create “Rainy platform”", +"生成 · {} 条": "Generation · {} takes", +"使用": "Used", +"赠送": "Gift", +"退回": "Refund", +"调整": "Adjustment", +"需要发票或退款,在每条充值记录里下载收据,或联系 support@inkseq.com。": "For invoices or refunds, download the receipt from a top-up entry or email support@inkseq.com.", +"充值 {}包": "Top up · {} pack", +"{} 积分(含多送 {}%)": "{} credits (incl. {}% bonus)", +"接下来会打开 Stripe 的安全付款页面。付款成功后积分马上到账,这个页面会自动刷新。税费按你所在地区在付款页显示。": "Stripe's secure checkout opens next. Credits arrive as soon as the payment succeeds and this page refreshes. Tax for your region is shown on the checkout page.", +"去付款": "Pay", +"原型:模拟付款成功": "Prototype: simulate a successful payment", +"已到账 {} 积分,收据已发到你的邮箱。": "{} credits added. A receipt was sent to your email.", +"好的": "OK", +"充值成功": "Top-up complete", +"积分不够": "Not enough credits", +"这次要用": "This needs", +",你还有": ", you have", +"。没有提交,也没有扣积分。": ". Nothing was submitted or charged.", +"可以少生成几条、换成快速草稿,或者先充值。": "Generate fewer takes, switch to Fast draft, or top up first.", +"返回修改": "Back", +"去充值": "Top up", +"登录方式": "Sign-in methods", +"邮箱": "Email", +"未连接": "Not connected", +"断开": "Disconnect", +"连接": "Connect", +"至少保留一种登录方式。邮箱用来收验证码和收据。": "Keep at least one sign-in method. Your email receives sign-in codes and receipts.", +"原型:已断开": "Prototype: disconnected", +"原型:跳转到 {} 授权": "Prototype: redirecting to {}", +"显示名字": "Display name", +"普通用户": "User", +"注销账号:作品和积分记录保留 30 天后删除,期间可以撤回。": "Delete account: your work and credit history are kept for 30 days, then deleted; you can cancel during that time.", +"申请注销": "Request deletion", +"原型:会先发确认邮件": "Prototype: a confirmation email is sent first", +"登录或注册": "Sign in or sign up", +"用一个账号做故事、快速创作,也能让你的 AI 助手帮你做。新用户验证邮箱后送": "One account for stories and Quick Create — and for your AI assistant. New users get", +"100 积分": "100 credits", +",够生成 5 条标准视频。": " after verifying their email — enough for 5 Standard videos.", +"用 Google 继续": "Continue with Google", +"用 Apple 继续": "Continue with Apple", +"用 GitHub 继续": "Continue with GitHub", +"或者用邮箱": "or use email", +"发送 6 位验证码": "Send a 6-digit code", +"验证码已发到": "We sent a code to", +",10 分钟内有效。": ". It is valid for 10 minutes.", +"换个邮箱": "Use another email", +"验证码": "Code", +"登录": "Sign in", +"我已阅读并同意": "I agree to the", +"用户协议": "Terms of Service", +"隐私政策": "Privacy Policy", +"和": "and", +"内容规则": "Content Policy", +"没有密码要记。同一个邮箱用 Google、Apple 或验证码登录,都是同一个账号。": "No password to remember. Google, Apple or an email code with the same address all reach the same account.", +"原型:任意方式都会直接登录为普通用户。": "Prototype: any method signs you in as a regular user.", +"预览身份": "Preview as", +"未登录": "Signed out", +"只有管理员能看到这一页。普通用户只看到积分,看不到美元、机器和利润。": "Only admins see this page. Regular users only see credits — never dollars, machines or margin.", +"用户": "Users", +"定价与利润": "Pricing & margin", +"测试机": "Test machines", +"操作记录": "Audit log", +"搜索名字或邮箱…": "Search name or email…", +"{} 个账号": "{} accounts", +"角色": "Role", +"30 天用量": "30-day usage", +"{} · {}加入": "{} · joined {}", +"调整积分": "Adjust credits", +"增减(负数为扣除)": "Change (negative to deduct)", +"原因(必填,会记录并显示给用户)": "Reason (required; logged and shown to the user)", +"例如:生成故障补偿 / 内测赠送": "e.g. compensation for a failed run / beta gift", +"确认": "Confirm", +"停用账号": "Suspend account", +"恢复账号": "Restore account", +"每档价格(积分 / 秒视频)": "Price per tier (credits per second of video)", +"1 积分 = US$0.01。改完先看下面的利润,再点「发布」;已经提交的任务按提交时的价格算。当前 v{}。": "1 credit = US$0.01. Check the margin below before you publish; submitted jobs keep the price they were quoted. Current v{}.", +"发布为 v{}": "Publish as v{}", +"每条 5 秒视频的利润": "Margin per 5-second clip", +"机器成本放大系数": "Machine cost multiplier", +"把开机、空转、失败重做算进去;看真实数据后调整": "Covers boot, idle time and failed reruns; tune it from real data", +"档位": "Tier", +"用户付": "User pays", +"纯机器": "GPU only", +"算上放大和手续费": "With multiplier and fees", +"毛利": "Margin", +"{} · {} 秒 · ${}/时": "{} · {} s · ${}/h", +"毛利低于 30% 标红。参考:MiniMax 官方 H3 约 $0.40–0.65 / 5 秒,Kling 约 $0.40–0.55,Runway 约 $0.25–0.60。": "Below 30% is red. For reference: MiniMax H3 official ≈ $0.40–0.65 per 5 s, Kling ≈ $0.40–0.55, Runway ≈ $0.25–0.60.", +"最近 30 天": "Last 30 days", +"用户用掉": "Credits used", +"= ${}": "= ${}", +"实际机器费用": "Actual machine cost", +"来自供应商账单": "from provider invoices", +"另有 $3.04 免除、$0.80 待对账": "plus $3.04 waived, $0.80 awaiting invoice", +"管理员操作记录": "Admin audit log", +"还没有操作。调整积分、改角色、发布定价都会记在这里,不能删除。": "Nothing yet. Credit adjustments, role changes and price publishes are logged here and cannot be deleted.", +"给 {} {} 积分 · 原因:{}": "Gave {} {} credits · reason: {}", +"扣 {} {} 积分 · 原因:{}": "Deducted {} {} credits from {} · reason: {}", +"{}:角色改为管理员": "{}: role changed to admin", +"{}:角色改为普通用户": "{}: role changed to user", +"{}:已停用": "{}: suspended", +"{}:已恢复": "{}: restored", +"发布定价 v{}:快速 {} · 标准 {} · 精修 {} 积分/秒": "Published pricing v{}: Fast {} · Standard {} · Refine {} credits/sec", +"这里的数都是默认值,改了立刻生效;不会有写死在后台、没地方改的限制。机器由系统按排队自动开关,普通用户不能开机。": "These are defaults and take effect immediately; no limit is hard-coded. Machines start and stop automatically from the queue; regular users cannot start machines.", +"密码": "Password", +"Google": "Google", +"不知道从哪开始?把故事交给 AI": "Not sure where to start? Hand your story to AI", +"贴一篇完整的故事或一段想法,AI 会拆好章节、人物和镜头,建好故事板直接带你进去。不生成视频就不花积分。": "Paste a whole story or an idea. AI splits it into chapters, characters and shots, builds the storyboard and takes you in. No credits are used until you generate video.", +"交给 AI,或从一句话、剧本、空白开始": "Hand it to AI, or start from a logline, a script or blank", +"推荐": "Recommended", +"交给 AI": "Hand it to AI", +"贴一篇完整的故事、小说片段或一段想法。AI 拆好章节、人物、地点和每个镜头,建好故事板直接带你进去。": "Paste a whole story, a novel excerpt or an idea. AI splits it into chapters, characters, locations and every shot, then opens the finished storyboard.", +"用我自己的 AI": "Use my own AI", +"Claude、Codex、ChatGPT 等助手连上你的账号,帮你写、帮你生成,你在网页上挑结果。": "Connect Claude, Codex, ChatGPT or another assistant to your account. It writes and generates; you pick results here.", +"或者自己来": "Or do it yourself", +"自动建好第一章和 4 个空镜头": "Creates chapter one with 4 empty shots", +"按“章 / 场 / 每行一个镜头”拆好": "Split by “chapter / scene / one shot per line”", +"会生成一段话,发给你的 AI 助手就能连上。它能做的事和你在网页上一样,花费有每日上限,随时可以暂停或作废。": "You get a short message to send to your AI assistant to connect it. It can do everything you can here, within a daily credit limit, and you can pause or revoke it any time.", +"连接我的 AI": "Connect my AI", +"(可留空,AI 会起一个)": "(optional — AI will name it)", +"你的故事": "Your story", +"把整篇故事贴进来,或者写几句你想拍什么。越具体,镜头越准。": "Paste the whole story, or a few lines about what you want to film. The more specific, the better the shots.", +"填入示例故事": "Use a sample story", +"{} / 50,000 字": "{} / 50,000 characters", +"让 AI 建故事板": "Build the storyboard with AI", +"AI 正在建故事板": "AI is building your storyboard", +"读懂故事": "Reading the story", +"拆成章节和场景": "Splitting into chapters and scenes", +"找出人物、地点和道具": "Finding characters, locations and props", +"写每个镜头的画面": "Writing each shot", +"建好故事板": "Building the storyboard", +"好了,正在打开…": "Done — opening…", +"一般 20–60 秒。只建故事板,不会生成视频,不花积分。": "Usually 20–60 seconds. This only builds the storyboard; no video is generated and no credits are used.", +"AI 建的故事": "Story from AI", +"全部文件夹": "All folders", +"文件夹": "Folders", +"按用到的地方自动归类": "sorted automatically by where files are used", +"上传了还没用": "Uploaded, not used yet", +"← 文件夹": "← Folders", +"所有快速创作用到的": "Everything used in Quick Create", +"还没放进任何作品": "Not in any work yet", +"刚上传,等你归类": "Just uploaded, waiting to be sorted", +"{} · {} 个文件 · {} 图 · {} 视频 · {} 声音": "{} · {} files · {} images · {} videos · {} audio", +"{} · {} 个文件": "{} · {} files", +"你把每日上限改成 {} 积分": "You changed the daily limit to {} credits", +"想生成 2 条,但会超过每日上限 {} 积分,没有提交": "Wanted to generate 2 takes, but that would exceed the {} credit daily limit — not submitted", +"读作品、写提示词、整理素材和档案;不生成,不花积分": "Reads work, writes prompts, organises files and profiles; no generation, no credits", +"还可以提交生成,用的积分不超过下面的每日上限": "Can also submit generations, within the daily credit limit below", +"今天已用 {} /": "Used today {} /", +"3. 范围:{};权限:{},每天最多用 {}。": "3. Scope: {}; permission: {}, at most {} per day.", +"我的积分": "Credits", +"大约够 {} 条 5 秒标准视频 · 积分不会过期": "Enough for about {} 5-second Standard videos · credits never expire", +"{} · {} · {}加入": "{} · {} · joined {}", +"· 已停用": "· suspended", +"继续即表示你同意": "By continuing you agree to the", +"。": "." +} diff --git a/design/inkseq-v2-prototype/src/home.jsx b/design/inkseq-v2-prototype/src/home.jsx index e2683fea..f76ac80d 100644 --- a/design/inkseq-v2-prototype/src/home.jsx +++ b/design/inkseq-v2-prototype/src/home.jsx @@ -1,9 +1,11 @@ -import React, {useCallback, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import {Popover} from './ui.jsx'; import {chosenVersion} from './model.js'; import {useStore} from './store.jsx'; import {AppHeader} from './pages.jsx'; -import {Clapperboard, LayoutGrid, ArrowRight, MessageSquare, MoreHorizontal, Pencil, Plus, Trash2} from 'lucide-react'; +import {Bot, Check, Clapperboard, LayoutGrid, ArrowRight, MessageSquare, MoreHorizontal, Pencil, Plus, Sparkles, Trash2} from 'lucide-react'; +import {AgentSheet} from './agent.jsx'; +import {AssistantHint} from './account.jsx'; import {running} from './trash.js'; const ago = t => { @@ -19,36 +21,91 @@ const SAMPLE_SCRIPT = `第一章 明天的信 旧录音机自己开始播放。 身后有人叫她的名字,林岚回头。`; -function NewStory({onClose}) { +// 交给 AI (prototype): in production POST /v1/story-imports runs an LLM plan → creates the storyboard through +// the same public API an external Agent uses → returns the project to open. Here a local splitter stands in. +function aiScript(text) { + const paras = text.split(/\n\s*\n|\r?\n/).map(x => x.trim()).filter(Boolean); + const out = []; + paras.forEach((p, i) => { + if (i % 3 === 0) out.push(`第${'一二三四五六七八九十'[Math.floor(i / 3)] || Math.floor(i / 3) + 1}章`); + const lines = p.split(/(?<=[。!?!?.])\s*/).map(x => x.trim()).filter(x => x.length > 1); + out.push(`场 ${i + 1} · ${(lines[0] || p).replace(/[,。!?,.!?].*$/, '').slice(0, 10)}`); + lines.slice(0, 6).forEach(l => out.push(l)); + }); + return out.join('\n'); +} +const STEPS = ['读懂故事', '拆成章节和场景', '找出人物、地点和道具', '写每个镜头的画面', '建好故事板']; +const SAMPLE_STORY = `深夜,林岚推开停运车站的大门。售票厅里只亮着一盏灯,柜台上放着一封写着她名字的信。 + +她拆开信,信上的日期是明天。落款是她自己。 + +失物间里的旧录音机突然开始播放,是她小时候的声音。身后有人叫她的名字,林岚回头。 + +站台上,最后一班车亮着灯缓缓进站。车门打开,里面坐着十年前的自己。`; + +function NewStory({onClose, start = 'ai'}) { const {dispatch} = useStore(); - const [mode, setMode] = useState('logline'); + const [mode, setMode] = useState(start); const [title, setTitle] = useState(''), [logline, setLogline] = useState(''), [aspect, setAspect] = useState('9:16'), - [style, setStyle] = useState(''), [script, setScript] = useState(''); - const modes = [['logline', '从一句话开始', '写一句故事,自动建好第一章和 4 个空镜头'], - ['script', '导入剧本', '粘贴剧本,按“章 / 场 / 每行一个镜头”自动拆好'], ['blank', '空白故事', '什么都不建,自己从头搭']]; + [style, setStyle] = useState(''), [script, setScript] = useState(''), [story, setStory] = useState(''); + const [step, setStep] = useState(-1), [agent, setAgent] = useState(false); + const own = [['logline', '从一句话开始', '自动建好第一章和 4 个空镜头'], ['script', '导入剧本', '按“章 / 场 / 每行一个镜头”拆好'], ['blank', '空白故事', '什么都不建,自己从头搭']]; const create = () => dispatch({type: 'newStory', mode, title: title || '未命名故事', logline, aspect, style, script}); + // Simulated import progress; the real one streams GET /v1/story-imports/{id} step by step. + useEffect(() => { + if (step < 0) return; + if (step >= STEPS.length) { const t = setTimeout(() => dispatch({type: 'newStory', mode: 'script', title: title || 'AI 建的故事', aspect, style, script: aiScript(story)}), 700); return () => clearTimeout(t); } + const t = setTimeout(() => setStep(step + 1), 650); + return () => clearTimeout(t); + }, [step]); // eslint-disable-line + if (agent) return ; + if (step >= 0) return ( +
+
AI 正在建故事板
+
{STEPS.map((l, i) =>
+ {i < step ? : i === step ? : }{l}
)}
+ {step >= STEPS.length ? '好了,正在打开…' : '一般 20–60 秒。只建故事板,不会生成视频,不花积分。'} +
); return (
e.stopPropagation()}>
新建故事
-
{modes.map(([k, l, d]) => ( - ))}
- - {mode === 'logline' &&