auto-save 2026-05-14 13:10 (~5)

This commit is contained in:
2026-05-14 13:10:42 +08:00
parent 69e73d44e6
commit 646f945fe9
5 changed files with 171 additions and 168 deletions

View File

@@ -521,8 +521,10 @@ export default function Home() {
const frame = job.frames.find((f) => f.index === frameIdx)
if (!frame) return
const productRefs = (shot.product_images ?? []).filter(Boolean).slice(0, 4) as ImageRef[]
if (!shot.first_image || !shot.last_image || productRefs.length < 4 || !shot.action_text?.trim()) {
toast.error("产品融合镜头缺少首帧、尾帧、固定产品图或描述词")
const subjectRefs = (shot.subject_images ?? []).filter(Boolean).slice(0, 7) as ImageRef[]
const primarySubject = shot.subject_image ?? subjectRefs[0] ?? null
if (!primarySubject || subjectRefs.length < 1 || productRefs.length < 4 || !shot.action_text?.trim()) {
toast.error("产品融合镜头缺少内置角色、固定产品图或描述词")
return
}
const duration = shot.duration && shot.duration > 0 ? shot.duration : 5
@@ -530,22 +532,21 @@ export default function Home() {
try {
const prompt = [
`产品融合镜头ID${shot.id || `shot-${frameIdx + 1}`}`,
`竖屏 9:16${duration.toFixed(1)}Seedance 图生视频。`,
"图片模型固定为 GPT Image 2首帧和尾帧已经由文字生图生成用来锁定透明骨架人角色、场景构图和动作起止状态。",
"视频模型固定为 Seedance使用首帧作为起始画面、尾帧作为结束画面并用四张同一 SKG 产品不同角度白底图作为垫图/产品身份参考。",
`首帧${labelOf(shot.first_image, "透明骨架人首帧")}。起始人物形象、姿态、构图和场景氛围以这张图为准`,
`尾帧:${labelOf(shot.last_image, "透明骨架人尾帧")}。结束人物状态、画面落点和场景延续以这张图为准。`,
`竖屏 9:16${duration.toFixed(1)}Seedance 参考图生视频。`,
"没有首帧和尾帧:请根据内置人物角色参考图、固定 SKG 产品图、场景/使用/享受描述直接生成完整视频。",
`人物角色:${shot.character_name || "透明骨架人"}。必须保持同一透明/半透明人体外壳、干净白色骨架、体型比例、服装风格和非恐怖广告气质。`,
`人物参考图${subjectRefs.map((ref, index) => `角色图${index + 1}=${labelOf(ref, "透明骨架人参考")}`).join("")}`,
`产品角度图 1${labelOf(productRefs[0], "SKG 产品正面/主视角")}`,
`产品角度图 2${labelOf(productRefs[1], "SKG 产品侧面/斜侧视角")}`,
`产品角度图 3${labelOf(productRefs[2], "SKG 产品背面/细节视角")}`,
`产品角度图 4${labelOf(productRefs[3], "SKG 产品补充/底部或佩戴视角")}`,
"产品使用部位:这是颈部/肩颈按摩仪,只能自然佩戴或贴合在脖子、后颈、颈肩交界处;不要放到手臂、腰、腿、胸口、眼部或背景里。",
"比例尺寸产品应符合真实颈部按摩仪大小U 形结构环绕后颈但不能巨大化、缩小成饰品、嵌入身体、悬浮或穿透透明人体。",
"镜头语言:严格按动作描述里的出场方式、景别、运镜、产品进入方式、佩戴贴合动作和收尾方式执行。",
`动作描述:${shot.action_text.trim()}`,
"镜头语言:严格按描述里的出场方式、场景、景别、运镜、产品进入方式、佩戴贴合动作、使用过程和收尾方式执行。",
`场景/使用/享受描述:${shot.action_text.trim()}`,
TRANSPARENT_HUMAN_VIDEO_PROMPT,
"融合要求:产品必须自然出现在透明骨架人动作中,尺寸可信,透视一致,只贴合手部拿取和后颈/颈肩使用区域,不能悬浮、漂移、融化、扭曲或变成其他物体。",
"首尾连续性:镜头从首帧自然运动到尾帧,中间不要跳切,不换角色,不换产品,不突然改变场景。",
"连续性:镜头必须完整连贯,中间不要跳切,不换角色,不换产品,不突然改变场景。",
"产品一致性:严格保持 SKG 产品外观、颜色、材质、U 形结构、按摩触点、按键和比例;四张产品角度图是产品身份真源。",
"场景要求:背景、空间、光线和阴影要自然统一,不要出现水印、平台 UI、字幕或竞品包装。",
"商业质感:真实拍摄感、干净高级、产品清楚可辨、人物动作自然、镜头稳定。",
@@ -555,11 +556,12 @@ export default function Home() {
const updated = await generateStoryboardVideo(job.id, frameIdx, {
prompt,
duration,
first_image: shot.first_image,
last_image: shot.last_image,
first_image: null,
last_image: null,
product_images: productRefs,
subject_image: shot.first_image,
scene_image: shot.last_image,
subject_image: primarySubject,
subject_images: subjectRefs,
scene_image: null,
product_image: productRefs[0] ?? null,
action_image: null,
source_ref: null,

View File

@@ -1,12 +1,13 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"
import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, RefreshCw, Plus, Sparkle, Crop, Copy, PencilLine, Trash2, Save, Upload, Play } from "lucide-react"
import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, RefreshCw, Plus, Sparkle, Crop, Copy, PencilLine, Trash2, Save, Play } from "lucide-react"
import {
frameUrl, cleanedFrameUrl, apiAssetUrl,
describeFrame, cleanupFrame, applyCleanedFrame, discardCleanedFrame, addElement, updateElement, deleteElement,
generateSceneAsset, generateSubjectAssets, resolveImageRefUrl, uploadStoryboardAsset, updateStoryboard, copyProductLibraryAsset,
type AssetBackground, type AssetSize, type KeyFrame, type Job, type ImageRef, type ProductFusionShot, type SceneAssetRole, type SceneStyle, type SubjectKind,
generateSceneAsset, generateSubjectAssets, resolveImageRefUrl, updateStoryboard, copyProductLibraryAsset,
listCharacterLibrary, copyCharacterLibraryAssets, characterLibraryImageUrl,
type AssetBackground, type AssetSize, type CharacterLibraryItem, type KeyFrame, type Job, type ImageRef, type ProductFusionShot, type SceneAssetRole, type SceneStyle, type SubjectKind,
} from "@/lib/api"
import { TRANSPARENT_HUMAN_FRAME_STANDARD, TRANSPARENT_HUMAN_UI_SUMMARY } from "@/lib/workflow-target"
import { toast } from "sonner"
@@ -114,6 +115,8 @@ const DESKTOP_PRODUCT_ANGLE_IDS = [
"desktop-skg-product-angle-03",
"desktop-skg-product-angle-04",
]
const DEFAULT_CHARACTER_ID = "character-01"
const DEFAULT_CHARACTER_NAME = "运动阳光男"
type FusionUploadTarget = {
shotIndex: number
slot: "first_image" | "last_image"
@@ -204,6 +207,10 @@ const createFusionShots = (): ProductFusionShot[] =>
last_image: null,
product_images: [],
product_image: null,
character_id: DEFAULT_CHARACTER_ID,
character_name: DEFAULT_CHARACTER_NAME,
subject_image: null,
subject_images: [],
person_image: null,
product_region: null,
scene_image: null,
@@ -223,6 +230,10 @@ const normalizeFusionShots = (shots?: ProductFusionShot[] | null): ProductFusion
...item,
...shot,
product_images: shot.product_images?.slice(0, PRODUCT_ANGLE_COUNT) ?? [],
character_id: shot.character_id || item.character_id,
character_name: shot.character_name || item.character_name,
subject_image: shot.subject_image ?? item.subject_image,
subject_images: shot.subject_images ?? item.subject_images,
action_text: shouldUseDefaultFusionDescription(shot.action_text) ? item.action_text : shot.action_text,
id: shot.id || item.id,
}
@@ -251,10 +262,11 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
const [activeTab, setActiveTab] = useState<LightboxTab>("clean")
const [fusionShots, setFusionShots] = useState<ProductFusionShot[]>(() => createFusionShots())
const [activeFusionShot, setActiveFusionShot] = useState(0)
const [fusionUploadTarget, setFusionUploadTarget] = useState<FusionUploadTarget | null>(null)
const [fusionGenerating, setFusionGenerating] = useState<number | "all" | null>(null)
const [fusionSaving, setFusionSaving] = useState(false)
const [fusionPresetPage, setFusionPresetPage] = useState(0)
const [characterLibrary, setCharacterLibrary] = useState<CharacterLibraryItem[]>([])
const [selectedCharacterId, setSelectedCharacterId] = useState(DEFAULT_CHARACTER_ID)
const [editingElement, setEditingElement] = useState<{
frameIndex: number
id: string
@@ -270,12 +282,23 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
const [draftRegion, setDraftRegion] = useState<Region | null>(null) // 当前正在拖的
const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null)
const imgWrapRef = useRef<HTMLDivElement>(null)
const fusionFileInputRef = useRef<HTMLInputElement | null>(null)
const loadedFusionKey = useRef("")
const activeIndexRef = useRef<number | null>(activeIndex)
useEffect(() => setMounted(true), [])
useEffect(() => { activeIndexRef.current = activeIndex }, [activeIndex])
useEffect(() => {
let cancelled = false
listCharacterLibrary()
.then((items) => {
if (!cancelled) setCharacterLibrary(items)
})
.catch((e) => {
if (!cancelled) toast.error("角色库读取失败:" + (e instanceof Error ? e.message : String(e)))
})
return () => { cancelled = true }
}, [])
useEffect(() => {
if (activeIndex === null) {
loadedFusionKey.current = ""
@@ -286,7 +309,9 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
const key = `${jobId}:${activeIndex}`
if (loadedFusionKey.current === key) return
const frame = frames.find((x) => x.index === activeIndex)
setFusionShots(normalizeFusionShots(frame?.storyboard?.product_fusion_shots as ProductFusionShot[] | undefined))
const nextShots = normalizeFusionShots(frame?.storyboard?.product_fusion_shots as ProductFusionShot[] | undefined)
setFusionShots(nextShots)
setSelectedCharacterId(nextShots[0]?.character_id || DEFAULT_CHARACTER_ID)
setActiveFusionShot(0)
loadedFusionKey.current = key
}, [activeIndex, frames, jobId])
@@ -391,9 +416,8 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
sceneExtraKeywords.trim() ? `额外关键词:${sceneExtraKeywords.trim()}` : "",
"要求:单一透明骨架人清晰可见,人物占画面主体,首尾帧可连续生成视频;无文字、水印、平台 UI、恐怖解剖感。",
].filter(Boolean).join("\n")
const fusionReadyCount = fusionShots.filter((shot) =>
shot.first_image && shot.last_image && shot.action_text?.trim()
).length
const fusionReadyCount = fusionShots.filter((shot) => shot.action_text?.trim()).length
const selectedCharacter = characterLibrary.find((item) => item.id === selectedCharacterId) ?? characterLibrary[0]
const persistFusionShots = async (nextShots: ProductFusionShot[]) => {
setFusionSaving(true)
@@ -425,47 +449,42 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
: { last_image: ref, guide_image: null }, true)
}
const uploadFusionFiles = async (files: FileList | File[]) => {
if (!fusionUploadTarget) return
const file = Array.from(files).find((item) => item.type.startsWith("image/"))
if (!file) {
toast.error("请上传图片文件")
return
}
try {
const ref = await uploadStoryboardAsset(jobId, file)
assignFusionImage(fusionUploadTarget, ref)
toast.success("已加入当前融合镜头")
} catch (e) {
toast.error("上传失败:" + (e instanceof Error ? e.message : String(e)))
} finally {
setFusionUploadTarget(null)
}
}
const openFusionUpload = (target: FusionUploadTarget) => {
setActiveFusionShot(target.shotIndex)
setFusionUploadTarget(target)
requestAnimationFrame(() => fusionFileInputRef.current?.click())
}
const ensureFixedProductAngles = async (indexes: number[]) => {
const prepareFusionReferences = async (indexes: number[]) => {
try {
const reusableRefs = fusionShots.find((shot) => (shot.product_images ?? []).filter(Boolean).length >= PRODUCT_ANGLE_COUNT)
?.product_images?.slice(0, PRODUCT_ANGLE_COUNT)
const refs = reusableRefs?.length === PRODUCT_ANGLE_COUNT
const productRefs = reusableRefs?.length === PRODUCT_ANGLE_COUNT
? reusableRefs
: await Promise.all(DESKTOP_PRODUCT_ANGLE_IDS.map((id) => copyProductLibraryAsset(jobId, id)))
const reusableSubjectRefs = fusionShots.find((shot) =>
shot.character_id === selectedCharacterId && (shot.subject_images ?? []).filter(Boolean).length > 0
)?.subject_images?.filter(Boolean)
const copiedCharacter = reusableSubjectRefs?.length
? {
character_id: selectedCharacterId,
character_name: selectedCharacter?.name || fusionShots.find((shot) => shot.character_id === selectedCharacterId)?.character_name || DEFAULT_CHARACTER_NAME,
images: reusableSubjectRefs,
}
: await copyCharacterLibraryAssets(jobId, selectedCharacterId)
const next = fusionShots.map((shot, index) => (
indexes.includes(index)
? { ...shot, product_images: refs, product_image: refs[0] ?? null, guide_image: null }
? {
...shot,
product_images: productRefs,
product_image: productRefs[0] ?? null,
character_id: copiedCharacter.character_id,
character_name: copiedCharacter.character_name,
subject_image: copiedCharacter.images[0] ?? null,
subject_images: copiedCharacter.images,
guide_image: null,
}
: shot
))
setFusionShots(next)
void persistFusionShots(next)
return next
} catch (e) {
toast.error("桌面产品角度填充失败:" + (e instanceof Error ? e.message : String(e)))
toast.error("内置角色/产品参考准备失败:" + (e instanceof Error ? e.message : String(e)))
return null
}
}
@@ -483,15 +502,29 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
toast.success(`已换第 ${Math.floor(start / FUSION_SHOT_COUNT) + 1} 组镜头语言`)
}
const selectFusionCharacter = (characterId: string) => {
const character = characterLibrary.find((item) => item.id === characterId)
const next = fusionShots.map((shot) => ({
...shot,
character_id: characterId,
character_name: character?.name || shot.character_name || DEFAULT_CHARACTER_NAME,
subject_image: null,
subject_images: [],
}))
setSelectedCharacterId(characterId)
setFusionShots(next)
void persistFusionShots(next)
}
const runFusionVideo = async (index: number) => {
const shot = fusionShots[index]
if (!shot?.first_image || !shot.last_image || !shot.action_text?.trim()) {
toast.error(`镜头 ${index + 1} 还缺首帧或尾帧`)
if (!shot?.action_text?.trim()) {
toast.error(`镜头 ${index + 1} 还缺场景/使用描述`)
return
}
setFusionGenerating(index)
try {
const next = await ensureFixedProductAngles([index])
const next = await prepareFusionReferences([index])
if (!next) return
await onGenerateProductFusionVideo?.(f.index, next[index])
} finally {
@@ -502,15 +535,15 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
const runAllFusionVideos = async () => {
const indexes = fusionShots
.map((shot, i) => ({ shot, i }))
.filter(({ shot }) => shot.first_image && shot.last_image && shot.action_text?.trim())
.filter(({ shot }) => shot.action_text?.trim())
.map(({ i }) => i)
if (indexes.length === 0) {
toast.error("还没有完整的融合镜头")
toast.error("还没有可生成的融合镜头")
return
}
setFusionGenerating("all")
try {
const next = await ensureFixedProductAngles(indexes)
const next = await prepareFusionReferences(indexes)
if (!next) return
for (const index of indexes) {
await onGenerateProductFusionVideo?.(f.index, next[index])
@@ -1034,23 +1067,7 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
</div>
</section>
) : isProductTab ? (
<section
className="rounded-lg border border-amber-300/15 bg-amber-500/[0.06] p-2.5"
onPaste={(e) => {
if (fusionUploadTarget && e.clipboardData.files?.length) void uploadFusionFiles(e.clipboardData.files)
}}
>
<input
ref={fusionFileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) => {
const files = e.target.files
if (files) void uploadFusionFiles(files)
e.currentTarget.value = ""
}}
/>
<section className="rounded-lg border border-amber-300/15 bg-amber-500/[0.06] p-2.5">
<div className="mb-2 flex items-center justify-between gap-2">
<div className="text-[12px] font-semibold text-white"></div>
<div className="flex items-center gap-1.5">
@@ -1062,7 +1079,7 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
onClick={rotateFusionDescriptions}
disabled={!!fusionGenerating}
className="inline-flex h-6 items-center justify-center gap-1 rounded bg-white/10 px-2 text-[9.5px] font-medium text-white/65 transition hover:bg-white/18 hover:text-white disabled:cursor-not-allowed disabled:opacity-40"
title="换一组内置镜头语言,不改变首帧和尾帧"
title="换一组内置镜头语言,不改变角色和视频结果"
>
<RefreshCw className="h-3 w-3" />
@@ -1078,77 +1095,51 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
</button>
</div>
</div>
<div className="mb-2 grid grid-cols-[minmax(220px,300px)_1fr] gap-2 rounded-md border border-white/10 bg-black/25 p-2">
<label className="block">
<div className="mb-1 text-[9px] text-white/38"></div>
<select
value={selectedCharacterId}
onChange={(e) => selectFusionCharacter(e.target.value)}
className="h-8 w-full rounded-md border border-white/10 bg-black/45 px-2 text-[10.5px] text-white/80 outline-none focus:border-amber-300/45"
>
{(characterLibrary.length ? characterLibrary : [{ id: DEFAULT_CHARACTER_ID, name: DEFAULT_CHARACTER_NAME } as CharacterLibraryItem]).map((character) => (
<option key={character.id} value={character.id}>{character.name}</option>
))}
</select>
</label>
<div className="min-w-0">
<div className="mb-1 flex items-center justify-between gap-2">
<span className="text-[9px] text-white/38"></span>
<span className="text-[8.5px] text-white/30"> 4 · </span>
</div>
<div className="flex gap-1.5 overflow-hidden">
{(selectedCharacter?.images ?? []).slice(0, 7).map((image) => (
<div key={image.id} className="h-12 w-10 overflow-hidden rounded border border-white/10 bg-black/35">
<img src={characterLibraryImageUrl(image.filename)} alt={image.label} className="h-full w-full object-cover" draggable={false} />
</div>
))}
{!selectedCharacter?.images?.length && (
<div className="flex h-12 items-center rounded border border-dashed border-white/10 px-2 text-[9.5px] text-white/32">
</div>
)}
</div>
</div>
</div>
<div className="mb-2 rounded-md border border-white/10 bg-black/25 px-2 py-1.5 text-[10px] leading-relaxed text-white/50">
使 4 SKG
/使
</div>
<div className="space-y-2">
{fusionShots.map((shot, i) => {
const active = i === activeFusionShot
const firstUrl = shot.first_image ? resolveImageRefUrl(jobId, shot.first_image) : ""
const lastUrl = shot.last_image ? resolveImageRefUrl(jobId, shot.last_image) : ""
const shotMarker = `${FUSION_PROMPT_MARKER_PREFIX}${shot.id}`
const shotVideos = generatedVideos.filter((video) => video.frame_idx === f.index && video.prompt.includes(shotMarker))
const latestShotVideo = shotVideos[0]
const latestVideoUrl = latestShotVideo?.url ? apiAssetUrl(latestShotVideo.url) : ""
const ready = !!(shot.first_image && shot.last_image && shot.action_text?.trim())
const ready = !!shot.action_text?.trim()
const busy = fusionGenerating === i || fusionGenerating === "all"
const lensStageLabel = PRODUCT_FUSION_LENS_STAGES[i] ?? `镜头 ${i + 1}`
const pasteIntoSlot = (target: FusionUploadTarget, label: string) => {
setActiveFusionShot(i)
if (clipboard) {
assignFusionImage(target, clipboard)
toast.success(`已粘贴到镜头 ${i + 1}${label}」:${clipboard.label || "剪贴板图片"}`)
return
}
setFusionUploadTarget(target)
toast.message(`镜头 ${i + 1} 已选中「${label}」槽位,现在可 Cmd+V 粘贴系统图片`)
}
const imageSlot = (target: FusionUploadTarget, label: string, url: string, ref?: ImageRef | null, white = false) => (
<div className="overflow-hidden rounded-md border border-white/10 bg-black/24">
<div className={`relative aspect-[4/5] ${white ? "bg-white" : "bg-black"}`}>
{url ? (
<button
type="button"
onClick={() => setActiveFusionShot(i)}
className="absolute inset-0 cursor-pointer"
title={`选中镜头 ${i + 1}`}
>
<img src={url} alt={label} className="h-full w-full object-contain" draggable={false} />
</button>
) : (
<button
type="button"
onClick={() => openFusionUpload(target)}
className={`absolute inset-0 flex flex-col items-center justify-center gap-1 text-[9.5px] ${white ? "text-black/35 hover:text-black/65" : "text-white/35 hover:text-white/65"}`}
>
<Upload className="h-3.5 w-3.5" />
{label}
</button>
)}
</div>
<div className="border-t border-white/10 px-1 py-1">
<div className="mb-1 truncate text-[8.5px] text-white/42">{ref?.label || label}</div>
<div className="grid grid-cols-2 gap-1">
<button
type="button"
onClick={() => pasteIntoSlot(target, label)}
className={`rounded px-1 py-0.5 text-[8.5px] transition ${
clipboard ? "bg-violet-500/60 text-white hover:bg-violet-400/70" : "bg-white/10 text-white/58 hover:bg-white/18 hover:text-white"
}`}
>
</button>
<button
type="button"
onClick={() => openFusionUpload(target)}
className="rounded bg-white/10 px-1 py-0.5 text-[8.5px] text-white/65 transition hover:bg-white/18 hover:text-white"
>
</button>
</div>
</div>
</div>
)
const resultPanel = latestShotVideo ? (
<div className="overflow-hidden rounded-md border border-white/10 bg-black/30">
<div className="relative aspect-video bg-black">
@@ -1200,7 +1191,7 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
: "border-white/10 bg-black/20 hover:border-amber-300/35"
}`}
>
<div className="grid grid-cols-[34px_92px_92px_minmax(220px,1fr)_78px_190px] items-start gap-2">
<div className="grid grid-cols-[34px_minmax(360px,1fr)_78px_190px] items-start gap-2">
<div className="flex flex-col items-center gap-1 pt-1">
<button
type="button"
@@ -1219,10 +1210,6 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
</span>
</div>
{imageSlot({ shotIndex: i, slot: "first_image" }, "首帧", firstUrl, shot.first_image)}
{imageSlot({ shotIndex: i, slot: "last_image" }, "尾帧", lastUrl, shot.last_image)}
<label className="block">
<div className="mb-1 flex items-center justify-between gap-2">
<span className="text-[9px] text-amber-100/65">{lensStageLabel}</span>
@@ -1236,7 +1223,7 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
const next = fusionShots.map((item, idx) => (idx === i ? { ...item, action_text: e.currentTarget.value } : item))
void persistFusionShots(next)
}}
placeholder="描述这个镜头里透明骨架人、SKG 产品和动作起止状态。"
placeholder="写清场景、产品如何佩戴到脖子/后颈,以及人物舒适享受状态。"
className="h-[92px] w-full resize-none rounded-md border border-white/10 bg-black/35 px-2 py-1.5 text-[10px] leading-relaxed text-white/75 outline-none placeholder:text-white/25 focus:border-amber-300/45"
/>
</label>