Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
634965f5f5 | ||
|
|
e62499c517 | ||
|
|
b6b3a9fed3 | ||
|
|
7bf63d0587 | ||
|
|
5cb75d5718 | ||
|
|
2304ab1538 | ||
|
|
8e84e4c21f |
+29
-18
@@ -1,19 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>影工坊</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&family=Noto+Sans+SC:wght@400;500;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script>
|
||||
(function () {
|
||||
var saved;
|
||||
try {
|
||||
saved = localStorage.getItem("aigc-theme");
|
||||
} catch (_) {}
|
||||
document.documentElement.dataset.theme =
|
||||
/^(night|day|grade|bluehour)$/.test(saved || "") ? saved : "night";
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="%sveltekit.assets%/themes.css" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>影工坊</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&family=Noto+Sans+SC:wght@400;500;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -59,6 +59,9 @@
|
||||
onCancel: (job: Job) => void;
|
||||
onSendToSlot: (path: string) => void;
|
||||
onDelete: (job: Job) => void;
|
||||
canAttach?: boolean;
|
||||
attachBusy?: boolean;
|
||||
onAttach?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -80,9 +83,13 @@
|
||||
onCancel,
|
||||
onSendToSlot,
|
||||
onDelete,
|
||||
canAttach = false,
|
||||
attachBusy = false,
|
||||
onAttach,
|
||||
}: Props = $props();
|
||||
|
||||
let detailsOpen = $state(false);
|
||||
let deleteOpen = $state(false);
|
||||
let outIndex = $state(0);
|
||||
let compare = $state(false),
|
||||
compareAt = $state(50),
|
||||
@@ -90,6 +97,7 @@
|
||||
$effect(() => {
|
||||
void job?.id;
|
||||
detailsOpen = false;
|
||||
deleteOpen = false;
|
||||
compare = false;
|
||||
lightbox = false;
|
||||
});
|
||||
@@ -132,16 +140,27 @@
|
||||
return live?.progress ?? 3;
|
||||
});
|
||||
let dims = $state('');
|
||||
let naturalWidth = $state(1),
|
||||
naturalHeight = $state(1);
|
||||
let stageWidth = $state(0),
|
||||
stageHeight = $state(0);
|
||||
const fitted = $derived(
|
||||
Math.min(stageWidth / naturalWidth, stageHeight / naturalHeight),
|
||||
);
|
||||
$effect.pre(() => {
|
||||
void output;
|
||||
dims = '';
|
||||
});
|
||||
function noteImg(e: Event) {
|
||||
const img = e.currentTarget as HTMLImageElement;
|
||||
naturalWidth = img.naturalWidth;
|
||||
naturalHeight = img.naturalHeight;
|
||||
dims = `${img.naturalWidth}×${img.naturalHeight}`;
|
||||
}
|
||||
function noteVideo(e: Event) {
|
||||
const v = e.currentTarget as HTMLVideoElement;
|
||||
naturalWidth = v.videoWidth;
|
||||
naturalHeight = v.videoHeight;
|
||||
dims = `${v.videoWidth}×${v.videoHeight}`;
|
||||
}
|
||||
|
||||
@@ -175,40 +194,29 @@
|
||||
</script>
|
||||
|
||||
<div class="artboard" aria-label="生成结果">
|
||||
<header class="artboard-toolbar">
|
||||
<button
|
||||
class="artboard-toolbar"
|
||||
aria-label="作品详情与操作"
|
||||
onclick={() => (detailsOpen = true)}
|
||||
>
|
||||
<div class="result-title">
|
||||
<span>第 {job?.id} 次生成</span><small>{wf?.name ?? '历史作品'}</small>
|
||||
</div>
|
||||
<div class="canvas-actions">
|
||||
{#if output && !video}<button
|
||||
class="canvas-btn"
|
||||
onclick={() => (lightbox = true)}
|
||||
aria-label="放大查看">⤢</button
|
||||
>
|
||||
{#if compareSource && compareSource !== output}<button
|
||||
class="canvas-btn"
|
||||
aria-pressed={compare}
|
||||
onclick={() => (compare = !compare)}
|
||||
>◫ {compare ? '结束对比' : '对比'}</button
|
||||
>{/if}
|
||||
{/if}
|
||||
{#if output}<a
|
||||
class="canvas-btn"
|
||||
href={assetUrl(output)}
|
||||
download
|
||||
aria-label="下载生成结果">↓ 下载</a
|
||||
>{/if}
|
||||
<button class="canvas-btn" onclick={() => (detailsOpen = true)}
|
||||
>详情与操作</button
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
</button>
|
||||
<div class="stage" class:comparing={compare}>
|
||||
{#if job && st}{#key mediaKey}
|
||||
<div class="stage-media">
|
||||
<div
|
||||
class="stage-media"
|
||||
bind:clientWidth={stageWidth}
|
||||
bind:clientHeight={stageHeight}
|
||||
>
|
||||
{#if output && video}
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<div class="print a-photo">
|
||||
<div
|
||||
class="print a-photo"
|
||||
style:width={fitted > 0 ? `${naturalWidth * fitted}px` : '100%'}
|
||||
style:height={fitted > 0 ? `${naturalHeight * fitted}px` : '100%'}
|
||||
>
|
||||
{#if st.tone === 'ok' && !fileGone}<span class="a-fix"
|
||||
>{t('jobst.done')}</span
|
||||
>{/if}
|
||||
@@ -217,6 +225,12 @@
|
||||
src={assetUrl(output)}
|
||||
controls
|
||||
aria-label="生成的视频,使用播放按钮开始播放"
|
||||
onkeydown={(e) => {
|
||||
if (e.code === 'Space' && e.target === e.currentTarget) {
|
||||
e.preventDefault();
|
||||
togglePlayback();
|
||||
}
|
||||
}}
|
||||
onplay={() => (paused = false)}
|
||||
onpause={() => (paused = true)}
|
||||
onended={() => (paused = true)}
|
||||
@@ -228,9 +242,21 @@
|
||||
}}
|
||||
onerror={() => output && markOutputMissing(output)}
|
||||
></video>
|
||||
{#if paused}<button
|
||||
class="big-play"
|
||||
aria-label="播放此视频"
|
||||
onclick={() => {
|
||||
videoEl?.focus();
|
||||
togglePlayback();
|
||||
}}>▶</button
|
||||
>{/if}
|
||||
</div>
|
||||
{:else if output}
|
||||
<div class="print a-photo">
|
||||
<div
|
||||
class="print a-photo"
|
||||
style:width={fitted > 0 ? `${naturalWidth * fitted}px` : '100%'}
|
||||
style:height={fitted > 0 ? `${naturalHeight * fitted}px` : '100%'}
|
||||
>
|
||||
{#if st.tone === 'ok' && !fileGone}<span class="a-fix"
|
||||
>{t('jobst.done')}</span
|
||||
>{/if}
|
||||
@@ -287,19 +313,72 @@
|
||||
</div>{/if}
|
||||
</div>
|
||||
<footer class="artboard-footer">
|
||||
{#if video}<button
|
||||
class="canvas-btn play-button"
|
||||
onclick={togglePlayback}
|
||||
aria-label={paused ? '播放视频' : '暂停视频'}
|
||||
>{paused ? '▶ 播放' : 'Ⅱ 暂停'}</button
|
||||
><button
|
||||
class="canvas-btn"
|
||||
onclick={() => videoEl?.requestFullscreen().catch(() => {})}
|
||||
>全屏</button
|
||||
>{/if}
|
||||
<span class="result-spec"
|
||||
>{dims || st?.label} · {job ? fmtDur(elapsedSec(job, nowMs)) : ''}</span
|
||||
<button
|
||||
class="canvas-btn"
|
||||
disabled={!output}
|
||||
onclick={() =>
|
||||
video
|
||||
? videoEl?.requestFullscreen().catch(() => {})
|
||||
: (lightbox = true)}
|
||||
aria-label="放大查看"><Icon name="zoom-in" size={14} />放大</button
|
||||
>
|
||||
<a
|
||||
class="canvas-btn"
|
||||
href={output ? assetUrl(output) : undefined}
|
||||
aria-disabled={!output}
|
||||
download
|
||||
aria-label="下载生成结果"><Icon name="download" size={14} />下载</a
|
||||
>
|
||||
<span class="action-separator" aria-hidden="true"></span>
|
||||
<button
|
||||
class="canvas-btn"
|
||||
disabled={!job || !output}
|
||||
onclick={() => job && onReuse(job)}
|
||||
><Icon name="retry" size={14} />复用参数</button
|
||||
>
|
||||
<button
|
||||
class="canvas-btn"
|
||||
disabled={!output || video || !canSendToSlot}
|
||||
title={canSendToSlot
|
||||
? '放入当前工作流图片输入'
|
||||
: '当前工作流没有图片输入'}
|
||||
onclick={() => output && onSendToSlot(output)}
|
||||
><Icon name="image" size={14} />作为参考图</button
|
||||
>
|
||||
<button
|
||||
class="canvas-btn"
|
||||
disabled
|
||||
title="跨工作流送图尚未具备,请选择生视频工作流后从已有结果选取"
|
||||
><Icon name="film" size={14} />送去生视频</button
|
||||
>
|
||||
<span class="action-separator" aria-hidden="true"></span>
|
||||
<button
|
||||
class="canvas-btn"
|
||||
disabled={!canAttach || attachBusy}
|
||||
title={canAttach
|
||||
? '保存到来源项目'
|
||||
: '通用选择项目与阶段将在后续批次提供'}
|
||||
onclick={onAttach}><Icon name="folder" size={14} />存入项目</button
|
||||
>
|
||||
<a class="canvas-btn" href="/assets"
|
||||
><Icon name="assets" size={14} />素材库</a
|
||||
>
|
||||
<button
|
||||
class="canvas-btn delete-output"
|
||||
aria-label="删除作品记录"
|
||||
title="删除作品记录"
|
||||
disabled={!job || job.status === 'pending' || job.status === 'running'}
|
||||
onclick={() => (deleteOpen = true)}
|
||||
><Icon name="close" size={14} /></button
|
||||
>
|
||||
{#if job?.status === 'pending' || job?.status === 'running'}<button
|
||||
class="canvas-btn"
|
||||
onclick={() => job && onCancel(job)}>取消生成</button
|
||||
>{/if}
|
||||
{#if st?.tone === 'fail'}<button
|
||||
class="canvas-btn"
|
||||
onclick={() => job && onRetry(job)}>重试</button
|
||||
>{/if}
|
||||
{#if compare && output && !video}<label class="compare-slider"
|
||||
>参考<input
|
||||
type="range"
|
||||
@@ -309,19 +388,41 @@
|
||||
aria-label="拖动对比画面"
|
||||
/>结果</label
|
||||
>{/if}
|
||||
{#if job?.status === 'pending' || job?.status === 'running'}<button
|
||||
class="canvas-btn"
|
||||
onclick={() => job && onCancel(job)}>取消生成</button
|
||||
>{/if}
|
||||
{#if st?.tone === 'fail'}<button
|
||||
class="canvas-btn"
|
||||
onclick={() => job && onRetry(job)}>重试</button
|
||||
>{/if}
|
||||
</footer>
|
||||
</div>
|
||||
<Modal bind:open={detailsOpen} size="lg" title="作品详情与操作">
|
||||
{#if job && st}
|
||||
<div class="detail-actions">
|
||||
{#if video}<button
|
||||
class="canvas-btn play-button"
|
||||
onclick={togglePlayback}
|
||||
aria-label={paused ? '播放视频' : '暂停视频'}
|
||||
>{paused ? '▶ 播放' : 'Ⅱ 暂停'}</button
|
||||
><button
|
||||
class="canvas-btn"
|
||||
onclick={() => videoEl?.requestFullscreen().catch(() => {})}
|
||||
>全屏</button
|
||||
>{/if}
|
||||
<span class="result-spec"
|
||||
>{dims || st?.label} · {job ? fmtDur(elapsedSec(job, nowMs)) : ''}</span
|
||||
>
|
||||
{#if compare && output && !video}<label class="compare-slider"
|
||||
>参考<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
bind:value={compareAt}
|
||||
aria-label="拖动对比画面"
|
||||
/>结果</label
|
||||
>{/if}
|
||||
{#if output && !video && compareSource && compareSource !== output}<button
|
||||
class="canvas-btn"
|
||||
aria-pressed={compare}
|
||||
onclick={() => {
|
||||
compare = !compare;
|
||||
detailsOpen = false;
|
||||
}}>◫ {compare ? '结束对比' : '对比'}</button
|
||||
>{/if}
|
||||
<button
|
||||
class="a-btn secondary sm"
|
||||
onclick={() => {
|
||||
@@ -348,7 +449,7 @@
|
||||
{#if job.status !== 'pending' && job.status !== 'running'}<button
|
||||
class="a-btn danger sm"
|
||||
onclick={() => {
|
||||
onDelete(job);
|
||||
deleteOpen = true;
|
||||
detailsOpen = false;
|
||||
}}>移除记录</button
|
||||
>{/if}
|
||||
@@ -409,6 +510,23 @@
|
||||
</dl>
|
||||
{/if}
|
||||
</Modal>
|
||||
<Modal bind:open={deleteOpen} title="删除作品记录" size="md">
|
||||
<p>
|
||||
确认移除这条生成记录及生成文件?关联项目中的回流记录也会移除。确认后有 5
|
||||
秒可撤销,提交后无法恢复。
|
||||
</p>
|
||||
{#snippet footer()}<button
|
||||
class="a-btn secondary"
|
||||
onclick={() => (deleteOpen = false)}>取消</button
|
||||
>
|
||||
<button
|
||||
class="a-btn danger"
|
||||
onclick={() => {
|
||||
if (job) onDelete(job);
|
||||
deleteOpen = false;
|
||||
}}>确认移除</button
|
||||
>{/snippet}
|
||||
</Modal>
|
||||
{#if lightbox && output && !video}<ImageLightbox
|
||||
src={assetUrl(output) ?? ''}
|
||||
alt={prompt || '创作结果'}
|
||||
@@ -417,13 +535,14 @@
|
||||
|
||||
<style>
|
||||
.artboard {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 38px minmax(0, 1fr) 32px;
|
||||
grid-template-rows: 0 minmax(0, 1fr) 40px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
border: 1px solid #ffffff0d;
|
||||
border-radius: 12px;
|
||||
background: radial-gradient(ellipse at 50% 20%, #202028, #131316 70%);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
.artboard-toolbar,
|
||||
@@ -431,10 +550,24 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
padding: 0 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.artboard-toolbar {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 2;
|
||||
border-radius: 4px;
|
||||
background: var(--media-scrim);
|
||||
color: var(--on-media);
|
||||
padding: 2px 6px;
|
||||
max-width: calc(100% - 20px);
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.result-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -451,25 +584,46 @@
|
||||
text-overflow: ellipsis;
|
||||
max-width: 220px;
|
||||
}
|
||||
.canvas-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.canvas-btn {
|
||||
background: #ffffff04;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 26px;
|
||||
box-sizing: border-box;
|
||||
background: var(--bg-2);
|
||||
color: var(--text-2);
|
||||
border: 1px solid #ffffff0a;
|
||||
border-radius: 6px;
|
||||
padding: 5px 8px;
|
||||
font-size: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--desk-action-radius);
|
||||
padding: 0 9px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.action-separator {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
margin: 0 4px;
|
||||
background: var(--line-2);
|
||||
}
|
||||
.delete-output {
|
||||
width: 26px;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
color: var(--err);
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
.canvas-btn:disabled,
|
||||
.canvas-btn[aria-disabled='true'] {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.canvas-btn:hover {
|
||||
color: var(--text);
|
||||
background: #ffffff0a;
|
||||
background: var(--bg-3);
|
||||
}
|
||||
.canvas-btn[aria-pressed='true'],
|
||||
.play-button {
|
||||
@@ -477,6 +631,7 @@
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.stage {
|
||||
grid-row: 2;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
@@ -485,7 +640,7 @@
|
||||
.stage-media {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 8px 20px;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 0;
|
||||
@@ -503,6 +658,20 @@
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.big-play {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--on-media-line);
|
||||
color: var(--on-media);
|
||||
background: var(--media-scrim);
|
||||
font-size: 26px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.print img,
|
||||
.print video {
|
||||
width: 100%;
|
||||
@@ -540,7 +709,7 @@
|
||||
.comparison-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #17171d;
|
||||
background: var(--bg-0);
|
||||
pointer-events: none;
|
||||
}
|
||||
.comparison-divider {
|
||||
@@ -552,7 +721,10 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
.artboard-footer {
|
||||
justify-content: flex-start;
|
||||
grid-row: 3;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
font-size: 10px;
|
||||
color: var(--text-3);
|
||||
}
|
||||
@@ -578,7 +750,7 @@
|
||||
gap: 4px;
|
||||
max-width: 90%;
|
||||
overflow: auto;
|
||||
background: #101015d9;
|
||||
background: var(--media-scrim);
|
||||
border: 1px solid var(--line);
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { createMutation, useQueryClient } from '@tanstack/svelte-query';
|
||||
import { workflows, apiErrorMessage } from '$lib/api';
|
||||
import type { WorkflowMeta } from '$lib/comfy/types';
|
||||
import { usageLabel } from '$lib/components/generate/schema';
|
||||
import { spotlight } from '$lib/actions/spotlight';
|
||||
import { toast } from '$lib/toast';
|
||||
import Icon from '$lib/components/ui/Icon.svelte';
|
||||
let {
|
||||
kind,
|
||||
initialUsage = 'all',
|
||||
metas,
|
||||
selectedId,
|
||||
pendingCount,
|
||||
@@ -15,9 +18,14 @@
|
||||
onKind,
|
||||
onDetails,
|
||||
disabled = false,
|
||||
historyCollapsed = false,
|
||||
onToggleHistory,
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
historyCollapsed?: boolean;
|
||||
onToggleHistory: () => void;
|
||||
kind: 'image' | 'video';
|
||||
initialUsage?: string;
|
||||
metas: WorkflowMeta[];
|
||||
selectedId: number | null;
|
||||
pendingCount: number;
|
||||
@@ -25,11 +33,47 @@
|
||||
onKind: (kind: 'image' | 'video') => void;
|
||||
onDetails: () => void;
|
||||
} = $props();
|
||||
let searchInput: HTMLInputElement;
|
||||
let searchOrigin: HTMLElement | null = null;
|
||||
function searchKeydown(e: KeyboardEvent) {
|
||||
if (e.defaultPrevented || e.isComposing || e.keyCode === 229) return;
|
||||
if (
|
||||
[
|
||||
...document.querySelectorAll('.shell-pop, .modal, [role="dialog"]'),
|
||||
].some((el) => el.getClientRects().length)
|
||||
)
|
||||
return;
|
||||
if (e.key === 'Escape' && e.target === searchInput) {
|
||||
e.preventDefault();
|
||||
const origin = searchOrigin?.isConnected
|
||||
? searchOrigin
|
||||
: searchInput.closest('aside')?.querySelector('button');
|
||||
if (origin instanceof HTMLElement) origin.focus();
|
||||
else searchInput.blur();
|
||||
return;
|
||||
}
|
||||
const el = e.target instanceof HTMLElement ? e.target : null;
|
||||
if (
|
||||
e.key !== '/' ||
|
||||
e.ctrlKey ||
|
||||
e.metaKey ||
|
||||
e.altKey ||
|
||||
el?.isContentEditable ||
|
||||
el?.closest(
|
||||
'input, textarea, select, [contenteditable]:not([contenteditable="false"])',
|
||||
) ||
|
||||
!searchInput?.getClientRects().length
|
||||
)
|
||||
return;
|
||||
e.preventDefault();
|
||||
searchInput.focus();
|
||||
}
|
||||
const client = useQueryClient();
|
||||
let search = $state(''),
|
||||
usage = $state('all'),
|
||||
usage = $state(untrack(() => initialUsage)),
|
||||
family = $state('all'),
|
||||
favOnly = $state(false),
|
||||
runnableOnly = $state(true),
|
||||
limit = $state(48),
|
||||
pins = $state<number[]>([]);
|
||||
onMount(() => {
|
||||
@@ -51,10 +95,18 @@
|
||||
metas
|
||||
.filter(
|
||||
(m) =>
|
||||
(!runnableOnly || runnable(m)) &&
|
||||
(!favOnly || m.favorite) &&
|
||||
(usage === 'all' || m.usage === usage) &&
|
||||
(family === 'all' || m.model_family === family) &&
|
||||
[m.name, m.description, m.model_family, ...(m.tags ?? [])]
|
||||
[
|
||||
m.name,
|
||||
m.description,
|
||||
m.source_path,
|
||||
m.usage ? usageLabel(m.usage) : null,
|
||||
m.model_family,
|
||||
...(m.tags ?? []),
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(search.trim().toLowerCase()),
|
||||
@@ -63,6 +115,10 @@
|
||||
(a, b) =>
|
||||
Number(pins.includes(b.id)) - Number(pins.includes(a.id)) ||
|
||||
Number(b.favorite) - Number(a.favorite) ||
|
||||
(a.model_family ?? '本地工作流').localeCompare(
|
||||
b.model_family ?? '本地工作流',
|
||||
'zh',
|
||||
) ||
|
||||
a.name.localeCompare(b.name, 'zh'),
|
||||
),
|
||||
);
|
||||
@@ -77,12 +133,23 @@
|
||||
const index = filtered.findIndex((m) => m.id === selectedId);
|
||||
if (index >= limit) limit = index + 24;
|
||||
});
|
||||
function runnable(m: WorkflowMeta) {
|
||||
return m.is_enabled && !!m.usage && !m.needs_mapping;
|
||||
}
|
||||
function choose(m: WorkflowMeta) {
|
||||
if (runnable(m)) onPick(m.id);
|
||||
else void goto('/workflows?manage=1&wf=' + m.id);
|
||||
}
|
||||
function pin(id: number) {
|
||||
pins = pins.includes(id) ? pins.filter((p) => p !== id) : [...pins, id];
|
||||
localStorage.setItem(
|
||||
'calliope.reference.workflowPins',
|
||||
JSON.stringify(pins),
|
||||
);
|
||||
try {
|
||||
localStorage.setItem(
|
||||
'calliope.reference.workflowPins',
|
||||
JSON.stringify(pins),
|
||||
);
|
||||
} catch {
|
||||
/* Pins remain usable this visit. */
|
||||
}
|
||||
}
|
||||
const favorite = createMutation({
|
||||
mutationFn: (m: WorkflowMeta) =>
|
||||
@@ -92,50 +159,89 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={searchKeydown} />
|
||||
|
||||
<aside class="workflow-rail" aria-label="创作工作流列表">
|
||||
<header>
|
||||
<div><strong>创作台</strong><small>选择工作流,开始创作</small></div>
|
||||
<strong>创作台</strong><button
|
||||
class="history-toggle a-btn ghost sm"
|
||||
onclick={onToggleHistory}
|
||||
aria-label={historyCollapsed ? '展开历史栏' : '收起历史栏'}
|
||||
aria-expanded={!historyCollapsed}>◫</button
|
||||
>
|
||||
</header>
|
||||
<div class="rail-tools">
|
||||
<div class="kind-tabs" role="tablist" aria-label="创作类型">
|
||||
<button
|
||||
role="tab"
|
||||
{disabled}
|
||||
aria-selected={kind === 'image'}
|
||||
onclick={() => onKind('image')}>图片</button
|
||||
><button
|
||||
role="tab"
|
||||
{disabled}
|
||||
onclick={() => onKind('image')}>图片</button
|
||||
>
|
||||
<button
|
||||
role="tab"
|
||||
aria-selected={kind === 'video'}
|
||||
{disabled}
|
||||
onclick={() => onKind('video')}>视频</button
|
||||
>
|
||||
</div>
|
||||
<input
|
||||
type="search"
|
||||
bind:value={search}
|
||||
aria-label="搜索工作流"
|
||||
placeholder="搜索工作流或用途…"
|
||||
/>
|
||||
<div class="filters">
|
||||
<select aria-label="筛选工作流用途" bind:value={usage}
|
||||
><option value="all">所有用途</option>{#each usages as u}<option
|
||||
value={u!}>{usageLabel(u!)}</option
|
||||
>{/each}</select
|
||||
><select aria-label="筛选模型家族" bind:value={family}
|
||||
><option value="all">全部模型</option>{#each families as f}<option
|
||||
value={f!}>{f}</option
|
||||
>{/each}</select
|
||||
<div class="rail-search">
|
||||
<Icon name="search" size={14} />
|
||||
<input
|
||||
type="search"
|
||||
bind:this={searchInput}
|
||||
onfocus={(e) => {
|
||||
searchOrigin =
|
||||
e.relatedTarget instanceof HTMLElement ? e.relatedTarget : null;
|
||||
}}
|
||||
bind:value={search}
|
||||
aria-label="搜索工作流"
|
||||
placeholder="搜用途、模型或文件名"
|
||||
/>
|
||||
<kbd>/</kbd>
|
||||
</div>
|
||||
<div class="filters" role="group" aria-label="筛选工作流用途">
|
||||
<button aria-pressed={usage === 'all'} onclick={() => (usage = 'all')}
|
||||
>全部</button
|
||||
>
|
||||
{#each usages as u}<button
|
||||
aria-pressed={usage === u}
|
||||
onclick={() => (usage = u!)}>{usageLabel(u!)}</button
|
||||
>{/each}
|
||||
<button aria-pressed={favOnly} onclick={() => (favOnly = !favOnly)}
|
||||
>☆ 收藏</button
|
||||
>
|
||||
</div>
|
||||
{#if families.length}<div
|
||||
class="filters"
|
||||
role="group"
|
||||
aria-label="筛选模型家族"
|
||||
>
|
||||
<button aria-pressed={family === 'all'} onclick={() => (family = 'all')}
|
||||
>全部模型</button
|
||||
>
|
||||
{#each families as f}<button
|
||||
aria-pressed={family === f}
|
||||
onclick={() => (family = f!)}>{f}</button
|
||||
>{/each}
|
||||
</div>{/if}
|
||||
<div class="list-meta">
|
||||
<span title="目录接口不提供仓库文件总数"
|
||||
>{filtered.length} 个 · 仓库文件数未知</span
|
||||
>
|
||||
<label class="runnable-filter"
|
||||
><input type="checkbox" bind:checked={runnableOnly} />仅可运行</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
<span>{filtered.length} 个可用工作流</span><button
|
||||
aria-pressed={favOnly}
|
||||
onclick={() => (favOnly = !favOnly)}>{favOnly ? '★' : '☆'} 收藏</button
|
||||
>
|
||||
</div>
|
||||
<div class="rail-list">
|
||||
{#each filtered.slice(0, limit) as m (m.id)}<div
|
||||
{#each filtered.slice(0, limit) as m, index (m.id)}
|
||||
{#if index === 0 || pins.includes(m.id) !== pins.includes(filtered[index - 1].id) || (!pins.includes(m.id) && m.model_family !== filtered[index - 1].model_family)}<div
|
||||
class="family-label"
|
||||
>
|
||||
{pins.includes(m.id) ? '置顶工作流' : m.model_family || '本地工作流'}
|
||||
</div>{/if}
|
||||
<div
|
||||
class="workflow-row reference-option"
|
||||
use:spotlight
|
||||
class:selected={selectedId === m.id}
|
||||
@@ -143,12 +249,15 @@
|
||||
<button
|
||||
class="workflow-pick"
|
||||
{disabled}
|
||||
onclick={() => onPick(m.id)}
|
||||
onclick={() => choose(m)}
|
||||
aria-pressed={selectedId === m.id}
|
||||
title={m.name}
|
||||
><i class:video={kind === 'video'}>{kind === 'image' ? '◈' : '▷'}</i
|
||||
><i class:video={kind === 'video'}
|
||||
>{m.model_family?.slice(0, 3) || (kind === 'image' ? '◈' : '▷')}</i
|
||||
><span
|
||||
><b>{m.name}</b><small
|
||||
><b>{m.name}</b>{#if !runnable(m)}<small class="pending"
|
||||
>待配置 · 映射 / 测试 →</small
|
||||
>{/if}<small
|
||||
>{m.description ||
|
||||
(m.usage ? usageLabel(m.usage) : '本地 ComfyUI')}</small
|
||||
></span
|
||||
@@ -180,7 +289,7 @@
|
||||
<footer>
|
||||
{#if pendingCount}<a href="/workflows?manage=1"
|
||||
>{pendingCount} 个工作流待配置 →</a
|
||||
>{/if}<button class="manage" onclick={onDetails}
|
||||
>{/if}<button class="manage" data-workflow-details onclick={onDetails}
|
||||
><span>+</span>
|
||||
<div>工作流说明与管理<small>输入要求 / 导入 / 分类</small></div>
|
||||
<b>›</b></button
|
||||
@@ -190,96 +299,172 @@
|
||||
|
||||
<style>
|
||||
.workflow-rail {
|
||||
width: 232px;
|
||||
box-sizing: border-box;
|
||||
width: var(--list);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: #121216;
|
||||
background: var(--bg-1);
|
||||
border-right: 1px solid var(--line);
|
||||
}
|
||||
.workflow-rail > header {
|
||||
padding: 21px 18px 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 12px 12px 0;
|
||||
height: var(--desk-rail-title-height);
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
header strong {
|
||||
font-size: 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
header small {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--text-3);
|
||||
margin-top: 6px;
|
||||
}
|
||||
.rail-tools {
|
||||
padding: 14px 12px 7px;
|
||||
padding: var(--desk-rail-gap) 12px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.kind-tabs {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
background: #09090c;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 9px;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
height: 36px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--desk-seg-radius);
|
||||
background: var(--well);
|
||||
}
|
||||
.kind-tabs button {
|
||||
flex: 1;
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
font-size: 12.5px;
|
||||
border: 0;
|
||||
padding: 7px 3px;
|
||||
background: none;
|
||||
color: var(--text-3);
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--desk-seg-selected-radius);
|
||||
background: transparent;
|
||||
color: var(--tx-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
.kind-tabs [aria-selected='true'] {
|
||||
color: #062f35;
|
||||
background: var(--accent);
|
||||
font-weight: 600;
|
||||
background: var(--bg-3);
|
||||
box-shadow: var(--sh-1);
|
||||
color: var(--tx-1);
|
||||
}
|
||||
.rail-tools input {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
font-size: 11px;
|
||||
padding: 9px 10px;
|
||||
background: #1d1b16;
|
||||
border: 1px solid #f5b04228;
|
||||
border-radius: 8px;
|
||||
.kind-tabs button:disabled {
|
||||
cursor: wait;
|
||||
}
|
||||
.rail-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: var(--desk-rail-gap);
|
||||
height: var(--desk-search-height);
|
||||
box-sizing: border-box;
|
||||
font-size: var(--desk-search-size);
|
||||
padding: 0 10px;
|
||||
background: var(--well);
|
||||
border: 1px solid var(--line-2);
|
||||
border-radius: var(--desk-search-radius);
|
||||
color: var(--text);
|
||||
}
|
||||
.rail-search input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
outline: none;
|
||||
}
|
||||
.rail-search kbd {
|
||||
font: var(--desk-search-key-font);
|
||||
padding: var(--desk-search-key-padding);
|
||||
border-radius: var(--desk-search-key-radius);
|
||||
border: 1px solid var(--line-2);
|
||||
color: var(--tx-3);
|
||||
background: var(--bg-1);
|
||||
}
|
||||
.rail-search:focus-within {
|
||||
border-color: var(--scene);
|
||||
}
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.filters select {
|
||||
width: 50%;
|
||||
min-width: 0;
|
||||
.filters button {
|
||||
height: var(--desk-filter-height);
|
||||
padding: 0 9px;
|
||||
font-size: var(--desk-filter-size);
|
||||
border-radius: var(--desk-filter-radius);
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg-3);
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
font-size: 10px;
|
||||
color: var(--text-3);
|
||||
background: var(--bg-2);
|
||||
color: var(--tx-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
.filters button[aria-pressed='true'] {
|
||||
color: var(--tx-1);
|
||||
border-color: color-mix(in srgb, var(--scene) 60%, transparent);
|
||||
background: color-mix(in srgb, var(--scene) 13%, var(--bg-2));
|
||||
}
|
||||
.runnable-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.runnable-filter input {
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
width: var(--desk-switch-width);
|
||||
height: var(--desk-switch-height);
|
||||
border: 0;
|
||||
border-radius: var(--r-pill);
|
||||
background: var(--bg-4);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.runnable-filter input::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
width: var(--desk-switch-knob);
|
||||
height: var(--desk-switch-knob);
|
||||
border-radius: 50%;
|
||||
background: var(--tx-2);
|
||||
transition: transform var(--t-base);
|
||||
}
|
||||
.runnable-filter input:checked {
|
||||
background: color-mix(in srgb, var(--scene) 55%, var(--bg-4));
|
||||
}
|
||||
.runnable-filter input:checked::after {
|
||||
transform: translateX(11px);
|
||||
background: var(--on-media);
|
||||
}
|
||||
.pending {
|
||||
color: var(--warn);
|
||||
}
|
||||
.list-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
font-size: 10px;
|
||||
color: var(--text-3);
|
||||
gap: 6px;
|
||||
padding: 8px 2px 0;
|
||||
font-size: var(--desk-filter-size);
|
||||
color: var(--tx-3);
|
||||
}
|
||||
.list-meta button {
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--text-3);
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.list-meta button[aria-pressed='true'] {
|
||||
color: var(--warn);
|
||||
.family-label {
|
||||
font-size: 11px;
|
||||
color: var(--tx-3);
|
||||
padding: 8px;
|
||||
}
|
||||
.rail-list {
|
||||
flex: 1;
|
||||
@@ -297,8 +482,12 @@
|
||||
margin: 2px 0;
|
||||
}
|
||||
.workflow-row.selected {
|
||||
border-color: #00cae039;
|
||||
background: linear-gradient(120deg, #00cae016, #00cae003);
|
||||
border-color: color-mix(in srgb, var(--scene) 35%, transparent);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
color-mix(in srgb, var(--scene) 12%, var(--bg-3)),
|
||||
var(--bg-3)
|
||||
);
|
||||
}
|
||||
.workflow-row.selected::before {
|
||||
content: '';
|
||||
@@ -306,7 +495,7 @@
|
||||
height: 22px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
background: var(--accent);
|
||||
background: var(--scene);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.workflow-pick {
|
||||
@@ -330,9 +519,9 @@
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 9px;
|
||||
border: 1px solid #00cae01f;
|
||||
color: var(--accent);
|
||||
background: #00cae008;
|
||||
border: 1px solid var(--line-2);
|
||||
color: var(--scene);
|
||||
background: var(--bg-3);
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -383,7 +572,7 @@
|
||||
opacity: 1;
|
||||
}
|
||||
.workflow-row:hover {
|
||||
background-color: #ffffff05;
|
||||
background-color: var(--bg-2);
|
||||
}
|
||||
.empty {
|
||||
color: var(--text-3);
|
||||
@@ -434,7 +623,7 @@
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.workflow-rail {
|
||||
width: 210px;
|
||||
width: var(--list);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<script lang="ts">
|
||||
import { assetUrl } from '$lib/api';
|
||||
import { jobStatusInfo } from '$lib/jobStatus';
|
||||
import { isVideoOutput } from '../generate/takes';
|
||||
import type { Job } from '$lib/comfy/types';
|
||||
let {
|
||||
jobs,
|
||||
selectedId,
|
||||
onSelect,
|
||||
onExpand,
|
||||
collapsed = false,
|
||||
}: {
|
||||
collapsed?: boolean;
|
||||
jobs: Job[];
|
||||
selectedId: number | null;
|
||||
onSelect: (job: Job) => void;
|
||||
onExpand: () => void;
|
||||
} = $props();
|
||||
const sorted = $derived([...jobs].sort((a, b) => b.id - a.id));
|
||||
</script>
|
||||
|
||||
<aside class="history-rail" class:collapsed aria-label="生成历史">
|
||||
<header>历史</header>
|
||||
{#if !collapsed}
|
||||
<div class="history-list">
|
||||
{#each sorted as job (job.id)}
|
||||
{@const status = jobStatusInfo(job)}
|
||||
<button
|
||||
class="history-take"
|
||||
class:selected={selectedId === job.id}
|
||||
onclick={() => onSelect(job)}
|
||||
aria-label={'查看历史 #' + job.id + ' · ' + status.label}
|
||||
aria-pressed={selectedId === job.id}
|
||||
>
|
||||
<div class="thumb">
|
||||
{#if job.output_paths[0] && status.tone === 'ok'}
|
||||
{#if isVideoOutput(job, job.output_paths[0])}<span
|
||||
class="video-mark">▷</span
|
||||
><video
|
||||
src={assetUrl(job.output_paths[0])}
|
||||
muted
|
||||
preload="metadata"
|
||||
aria-label="视频缩略图"
|
||||
></video>
|
||||
{:else}<img
|
||||
src={assetUrl(job.output_paths[0])}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
/>{/if}
|
||||
{:else}<span class:failed={status.tone === 'fail'}
|
||||
>{status.label}</span
|
||||
>{/if}
|
||||
</div>
|
||||
<small>#{job.id}</small>
|
||||
</button>
|
||||
{/each}
|
||||
{#if !sorted.length}<p>生成后<br />记录留在这里</p>{/if}
|
||||
</div>
|
||||
<footer><button onclick={onExpand}>全部记录</button></footer>
|
||||
{/if}
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
.history-rail {
|
||||
box-sizing: border-box;
|
||||
width: var(--hist);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
border-left: 1px solid var(--line);
|
||||
background: var(--bg-1);
|
||||
}
|
||||
.history-rail.collapsed {
|
||||
width: 0;
|
||||
border: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
header {
|
||||
height: var(--head);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
button {
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
background: none;
|
||||
color: var(--tx-2);
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.history-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 8px 6px;
|
||||
}
|
||||
.history-take {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 3px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--r-sm);
|
||||
}
|
||||
.history-take.selected {
|
||||
border-color: var(--scene);
|
||||
background: color-mix(in srgb, var(--scene) 10%, transparent);
|
||||
}
|
||||
.thumb {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--bg-0);
|
||||
border-radius: var(--r-xs);
|
||||
overflow: hidden;
|
||||
font-size: 10px;
|
||||
}
|
||||
img,
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.video-mark {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
color: var(--on-media);
|
||||
font-size: 24px;
|
||||
text-shadow: 0 1px 6px var(--media-scrim);
|
||||
}
|
||||
.failed {
|
||||
color: var(--err);
|
||||
}
|
||||
small {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
color: var(--tx-3);
|
||||
}
|
||||
p {
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
color: var(--tx-3);
|
||||
line-height: 1.7;
|
||||
}
|
||||
footer {
|
||||
padding: 10px 2px;
|
||||
text-align: center;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { readDeskDraft, writeDeskDraft } from './draftCache';
|
||||
import HistoryRail from './HistoryRail.svelte';
|
||||
import { createQuery, useQueryClient } from '@tanstack/svelte-query';
|
||||
import { toStore } from 'svelte/store';
|
||||
import { tick, untrack } from 'svelte';
|
||||
@@ -263,6 +265,8 @@
|
||||
let paramEndpoint = $state('');
|
||||
let paramShowErrors = $state(false);
|
||||
let historyOpen = $state(false);
|
||||
let historyCollapsed = $state(false);
|
||||
let dockAnchor = $state<HTMLElement | null>(null);
|
||||
let focusMode = $state(false);
|
||||
let bayActiveId = $state<number | null>(null);
|
||||
let bay = $state<WorkflowBay | null>(null);
|
||||
@@ -276,7 +280,9 @@
|
||||
function closeBay() {
|
||||
bayOpen = false;
|
||||
void tick().then(() =>
|
||||
document.querySelector<HTMLButtonElement>('.model-selector')?.focus(),
|
||||
document
|
||||
.querySelector<HTMLButtonElement>('[data-workflow-details]')
|
||||
?.focus(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -286,6 +292,7 @@
|
||||
if (slotBusy || submitting) return;
|
||||
actionRevision++;
|
||||
selectedJobId = workflowResults.get(id) ?? null;
|
||||
historyJobId = null;
|
||||
if (id !== workflowId) workflowId = id;
|
||||
void tick().then(() => promptEl?.focus());
|
||||
}
|
||||
@@ -308,7 +315,10 @@
|
||||
workflowId = restoredWorkflowId;
|
||||
return;
|
||||
}
|
||||
if (deskMetas.some((m) => m.id === raw)) workflowId = raw;
|
||||
if (deskMetas.some((m) => m.id === raw)) {
|
||||
if (pendingDraft?.workflowId !== raw) pendingDraft = null;
|
||||
workflowId = raw;
|
||||
}
|
||||
});
|
||||
|
||||
async function toggleFavorite(id: number, fav: boolean) {
|
||||
@@ -335,7 +345,6 @@
|
||||
draftContext?.projectId,
|
||||
draftContext?.stage,
|
||||
draftContext?.sourceId ?? draftContext?.label,
|
||||
page.url.searchParams.get('wf'),
|
||||
]),
|
||||
);
|
||||
const sourceFingerprint = JSON.stringify([
|
||||
@@ -343,9 +352,13 @@
|
||||
draftContext?.negative,
|
||||
draftContext?.durationSec,
|
||||
]);
|
||||
function readDraft(): { workflowId: number; values: FormValues } | null {
|
||||
function readDraft(): {
|
||||
workflowId: number;
|
||||
values: FormValues;
|
||||
requestId?: string | null;
|
||||
} | null {
|
||||
try {
|
||||
const d = JSON.parse(window.sessionStorage.getItem(draftKey) ?? 'null');
|
||||
const d = JSON.parse(readDeskDraft(draftKey) ?? 'null');
|
||||
if (
|
||||
!d ||
|
||||
d.source !== sourceFingerprint ||
|
||||
@@ -367,19 +380,27 @@
|
||||
)
|
||||
clean[key] = value;
|
||||
}
|
||||
return { workflowId: d.workflowId, values: clean };
|
||||
return {
|
||||
workflowId: d.workflowId,
|
||||
values: clean,
|
||||
requestId: d.requestId,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
let pendingDraft = untrack(readDraft);
|
||||
// Keep the reload decision stable while detail and metadata queries arrive in either order.
|
||||
const restoredWorkflowId = untrack(() =>
|
||||
pendingDraft?.requestId === page.url.searchParams.get('wf')
|
||||
? pendingDraft?.workflowId
|
||||
: null,
|
||||
);
|
||||
const workflowDrafts = new SvelteMap<number, FormValues>();
|
||||
const workflowEndpoints = new SvelteMap<number, string>();
|
||||
const workflowResults = new SvelteMap<number, number>();
|
||||
try {
|
||||
const saved = JSON.parse(
|
||||
window.sessionStorage.getItem(draftKey + ':workflows') ?? 'null',
|
||||
);
|
||||
const saved = JSON.parse(readDeskDraft(draftKey + ':workflows') ?? 'null');
|
||||
if (saved?.source === sourceFingerprint) {
|
||||
for (const [id, draft] of Object.entries(saved.drafts ?? {}))
|
||||
if (Number.isInteger(Number(id)) && draft && typeof draft === 'object')
|
||||
@@ -390,7 +411,7 @@
|
||||
} catch {
|
||||
/* The form works without storage. */
|
||||
}
|
||||
const restoredWorkflowId = pendingDraft?.workflowId ?? null;
|
||||
|
||||
let workflowId = $state<number | null>(pendingDraft?.workflowId ?? null);
|
||||
let values = $state<FormValues>({});
|
||||
let showErrors = $state(false);
|
||||
@@ -456,6 +477,16 @@
|
||||
...schemaDefaults(wf.input_schema ?? []),
|
||||
...(workflowDrafts.get(wf.id) ?? {}),
|
||||
};
|
||||
if (lastWf && !workflowDrafts.has(wf.id)) {
|
||||
const previous = classifySchema(lastWf.input_schema ?? [], lastWf.kind);
|
||||
const incoming = classifySchema(wf.input_schema ?? [], wf.kind);
|
||||
for (const role of ['prompt', 'negative'] as const) {
|
||||
const oldInput = previous[role],
|
||||
newInput = incoming[role];
|
||||
if (oldInput && newInput && values[oldInput.nodeId] !== undefined)
|
||||
next[newInput.nodeId] = values[oldInput.nodeId];
|
||||
}
|
||||
}
|
||||
endpointTouched = true;
|
||||
endpointId =
|
||||
workflowEndpoints.get(wf.id) ?? wf.endpoint_id ?? activeId ?? '';
|
||||
@@ -484,7 +515,7 @@
|
||||
try {
|
||||
workflowDrafts.set(wf.id, structuredClone($state.snapshot(inputs)));
|
||||
workflowEndpoints.set(wf.id, ep);
|
||||
window.sessionStorage.setItem(
|
||||
writeDeskDraft(
|
||||
draftKey + ':workflows',
|
||||
JSON.stringify({
|
||||
source: sourceFingerprint,
|
||||
@@ -492,10 +523,11 @@
|
||||
endpoints: Object.fromEntries(workflowEndpoints),
|
||||
}),
|
||||
);
|
||||
window.sessionStorage.setItem(
|
||||
writeDeskDraft(
|
||||
draftKey,
|
||||
JSON.stringify({
|
||||
workflowId: wf.id,
|
||||
requestId: page.url.searchParams.get('wf'),
|
||||
values: inputs,
|
||||
source: sourceFingerprint,
|
||||
}),
|
||||
@@ -671,6 +703,7 @@
|
||||
return;
|
||||
}
|
||||
const wf = currentWorkflow!;
|
||||
historyJobId = null;
|
||||
submitting = true;
|
||||
try {
|
||||
const res = await playgroundApi.generate({
|
||||
@@ -710,6 +743,7 @@
|
||||
// ------------------------------------------------------------------
|
||||
let filter = $state<FeedFilter>(untrack(() => kind));
|
||||
let selectedJobId = $state<number | null>(null);
|
||||
let historyJobId = $state<number | null>(null);
|
||||
let busyId = $state<number | null>(null);
|
||||
let tab = $state<'result' | 'params'>('result');
|
||||
let inspOff = $state(false);
|
||||
@@ -747,7 +781,8 @@
|
||||
|
||||
/** Picked take, else the newest finished one in view (undo-pending rows don't count). */
|
||||
const selectedJob = $derived(
|
||||
allJobs.find((j) => j.id === selectedJobId && !pendingIds.has(j.id)) ??
|
||||
allJobs.find((j) => j.id === historyJobId && !pendingIds.has(j.id)) ??
|
||||
allJobs.find((j) => j.id === selectedJobId && !pendingIds.has(j.id)) ??
|
||||
allJobs
|
||||
.filter(
|
||||
(j) =>
|
||||
@@ -766,9 +801,8 @@
|
||||
*/
|
||||
function selectTake(job: Job, via: 'pointer' | 'keyboard' = 'pointer') {
|
||||
const from = via === 'pointer' ? (feed?.markRect() ?? null) : null;
|
||||
selectedJobId = job.id;
|
||||
if (workflowId != null && job.workflow_id === workflowId)
|
||||
workflowResults.set(workflowId, job.id);
|
||||
historyJobId = job.id;
|
||||
|
||||
tab = 'result';
|
||||
crossfade = via === 'pointer';
|
||||
if (via === 'pointer') {
|
||||
@@ -843,10 +877,11 @@
|
||||
function mediumLink(next: DeskKind) {
|
||||
if (slotBusy || submitting) return;
|
||||
const url = new URL(page.url);
|
||||
url.pathname = '/' + next;
|
||||
url.pathname = '/create';
|
||||
url.searchParams.set('kind', next);
|
||||
url.searchParams.delete('wf');
|
||||
url.searchParams.delete('mode');
|
||||
goto(url.pathname + url.search);
|
||||
goto(url.pathname + url.search + url.hash);
|
||||
}
|
||||
|
||||
const imageSlotTarget = $derived(
|
||||
@@ -898,15 +933,60 @@
|
||||
next[k] = v as string[];
|
||||
}
|
||||
if (slotBusy || submitting || revision !== actionRevision) return;
|
||||
const stampedEndpoint = job.payload?.endpoint_id;
|
||||
const reuseEndpoint =
|
||||
typeof stampedEndpoint === 'string' &&
|
||||
endpoints.some((ep) => ep.id === stampedEndpoint)
|
||||
? stampedEndpoint
|
||||
: (wf.endpoint_id ?? activeId ?? '');
|
||||
if (wf.kind !== kind) {
|
||||
const parts = JSON.parse(draftKey.slice('creative-draft:'.length));
|
||||
parts[0] = wf.kind;
|
||||
const targetKey = 'creative-draft:' + JSON.stringify(parts);
|
||||
let saved: {
|
||||
source?: string;
|
||||
drafts?: Record<string, FormValues>;
|
||||
endpoints?: Record<string, string>;
|
||||
} = {};
|
||||
try {
|
||||
saved =
|
||||
JSON.parse(readDeskDraft(targetKey + ':workflows') ?? '{}') ?? {};
|
||||
} catch {
|
||||
/* No prior draft. */
|
||||
}
|
||||
if (saved.source !== sourceFingerprint) saved = {};
|
||||
writeDeskDraft(
|
||||
targetKey + ':workflows',
|
||||
JSON.stringify({
|
||||
source: sourceFingerprint,
|
||||
drafts: { ...saved.drafts, [wf.id]: next },
|
||||
endpoints: { ...saved.endpoints, [wf.id]: reuseEndpoint },
|
||||
}),
|
||||
);
|
||||
writeDeskDraft(
|
||||
targetKey,
|
||||
JSON.stringify({
|
||||
workflowId: wf.id,
|
||||
requestId: String(wf.id),
|
||||
values: next,
|
||||
source: sourceFingerprint,
|
||||
}),
|
||||
);
|
||||
const url = new URL(page.url);
|
||||
url.searchParams.set('kind', wf.kind);
|
||||
url.searchParams.set('wf', String(wf.id));
|
||||
await goto('/create' + url.search + url.hash);
|
||||
return;
|
||||
}
|
||||
if (lastWf) workflowEndpoints.set(lastWf.id, endpointId);
|
||||
endpointTouched = true;
|
||||
endpointId =
|
||||
workflowEndpoints.get(wf.id) ?? wf.endpoint_id ?? activeId ?? '';
|
||||
endpointId = reuseEndpoint;
|
||||
if (lastWf && lastWf.id !== wf.id)
|
||||
workflowDrafts.set(lastWf.id, structuredClone($state.snapshot(values)));
|
||||
workflowDrafts.set(wf.id, next);
|
||||
workflowId = wf.id;
|
||||
selectedJobId = job.id;
|
||||
historyJobId = null;
|
||||
workflowResults.set(wf.id, job.id);
|
||||
lastWf = wf;
|
||||
values = next;
|
||||
@@ -1104,10 +1184,17 @@
|
||||
{/snippet}
|
||||
|
||||
<AppShell active={kind} title="创作台" workspace>
|
||||
<div class="reference-desk" class:focused={focusMode}>
|
||||
<div
|
||||
class="reference-desk"
|
||||
class:focused={focusMode}
|
||||
style:--scene={kind === 'image' ? 'var(--sc-image)' : 'var(--sc-video)'}
|
||||
>
|
||||
<DeskWorkflowRail
|
||||
{kind}
|
||||
metas={deskMetas}
|
||||
initialUsage={mode}
|
||||
{historyCollapsed}
|
||||
onToggleHistory={() => (historyCollapsed = !historyCollapsed)}
|
||||
metas={allMetas.filter((m) => m.kind === kind)}
|
||||
selectedId={workflowId}
|
||||
{pendingCount}
|
||||
onPick={pickWorkflow}
|
||||
@@ -1117,28 +1204,53 @@
|
||||
/>
|
||||
<div class="refract-studio">
|
||||
<header class="studio-heading">
|
||||
<div class="heading-actions">
|
||||
<button class="a-btn ghost sm" onclick={() => (historyOpen = true)}
|
||||
>◷ 生成历史</button
|
||||
><button id="desk-wf" class="model-selector" onclick={openBay}
|
||||
>{currentWorkflow?.name ?? currentMeta?.name ?? '选择工作流'} ⌄</button
|
||||
<div class="heading-actions workflow-identity">
|
||||
<span class="fam-dot"
|
||||
>{currentMeta?.model_family?.slice(0, 3) ??
|
||||
(kind === 'image' ? '◈' : '▷')}</span
|
||||
>
|
||||
</div>
|
||||
<div class="heading-actions">
|
||||
{#if selectedJob && workbenchCtxOf(selectedJob) && selectedJob.status === 'done'}<button
|
||||
class="a-btn secondary sm"
|
||||
disabled={attachBusyId === selectedJob.id}
|
||||
onclick={() => selectedJob && attachToProject(selectedJob)}
|
||||
>保存到来源项目 ↗</button
|
||||
<b id="desk-wf" class="model-selector"
|
||||
>{currentWorkflow?.name ?? currentMeta?.name ?? '选择工作流'}</b
|
||||
>
|
||||
{#if currentMeta?.usage}<span class="a-tag"
|
||||
>{usageLabel(currentMeta.usage)}</span
|
||||
>{/if}
|
||||
<button class="a-btn secondary sm" onclick={openParams}
|
||||
>创作设置</button
|
||||
><button class="a-btn ghost sm" onclick={toggleInspector}
|
||||
>{focusMode ? '退出专注' : '专注创作'} ⤢</button
|
||||
</div>
|
||||
<span
|
||||
class="workflow-file"
|
||||
title={currentMeta?.source_path ?? '此工作流未记录源文件名'}
|
||||
>{currentMeta?.source_path?.replaceAll('\\', '/').split('/').pop() ??
|
||||
'源文件名未知'}</span
|
||||
>
|
||||
<div class="heading-actions">
|
||||
<span
|
||||
class="connection-status"
|
||||
class:failed={connState.state === 'fail'}
|
||||
>
|
||||
<i class:connected={connState.state === 'ok'}></i>
|
||||
{endpoints.find((e) => e.id === endpointId)?.name ?? '未选择连接'} · {connState.state ===
|
||||
'ok' && connState.ms != null
|
||||
? `${connState.ms}ms`
|
||||
: connState.state === 'fail'
|
||||
? '连接失败'
|
||||
: '延迟未知'}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
{@render ctxBanner()}
|
||||
{#if historyJobId != null && selectedJob?.id === historyJobId}<div
|
||||
class="history-banner"
|
||||
role="status"
|
||||
>
|
||||
<span>正在查看历史 #{historyJobId} · 当前输入保持不变</span><button
|
||||
class="a-btn ghost sm"
|
||||
onclick={() => (historyJobId = null)}>返回当前创作</button
|
||||
><button
|
||||
class="a-btn secondary sm"
|
||||
onclick={() => selectedJob && reuse(selectedJob)}
|
||||
>复用到输入区</button
|
||||
>
|
||||
</div>{/if}
|
||||
<div class="studio-layout">
|
||||
<section class="viewer" aria-label="查看生成结果">
|
||||
<div class="preview-area">
|
||||
@@ -1157,6 +1269,11 @@
|
||||
open={true}
|
||||
inactive={false}
|
||||
canSendToSlot={!!imageSlotTarget || !!imageList}
|
||||
canAttach={!!selectedJob &&
|
||||
!!workbenchCtxOf(selectedJob) &&
|
||||
selectedJob.status === 'done'}
|
||||
attachBusy={attachBusyId === selectedJob?.id}
|
||||
onAttach={() => selectedJob && attachToProject(selectedJob)}
|
||||
{crossfade}
|
||||
bind:videoEl
|
||||
params={paramsTab}
|
||||
@@ -1186,7 +1303,11 @@
|
||||
</div>{/if}
|
||||
</div>
|
||||
</section>
|
||||
<aside class="studio-form" aria-label="提示词、素材与参数">
|
||||
<aside
|
||||
bind:this={dockAnchor}
|
||||
class="studio-form"
|
||||
aria-label="提示词、素材与参数"
|
||||
>
|
||||
<Composer
|
||||
bind:this={composer}
|
||||
dock
|
||||
@@ -1219,6 +1340,13 @@
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
<HistoryRail
|
||||
collapsed={historyCollapsed}
|
||||
jobs={allJobs.filter((j) => j.kind === kind && !pendingIds.has(j.id))}
|
||||
selectedId={selectedJob?.id ?? null}
|
||||
onSelect={selectTake}
|
||||
onExpand={() => (historyOpen = true)}
|
||||
/>
|
||||
</div>
|
||||
</AppShell>
|
||||
<Modal bind:open={bayOpen} size="xl" title="工作流说明与管理"
|
||||
@@ -1332,17 +1460,25 @@
|
||||
</Modal>
|
||||
<Modal
|
||||
bind:open={paramsOpen}
|
||||
anchor={dockAnchor}
|
||||
size="lg"
|
||||
title="创作设置"
|
||||
onclose={() => (paramDraft = null)}
|
||||
>
|
||||
{#snippet headerActions()}<button
|
||||
class="a-btn ghost sm"
|
||||
onclick={resetParamDraft}>恢复工作流默认</button
|
||||
>{/snippet}
|
||||
{#snippet footer()}<div class="param-actions">
|
||||
<span>保存后应用到当前工作流</span><button
|
||||
class="a-btn secondary"
|
||||
onclick={() => (paramsOpen = false)}>取消</button
|
||||
><button class="a-btn purple" onclick={saveParams}>保存设置</button>
|
||||
><button class="a-btn primary" onclick={saveParams}>保存设置</button>
|
||||
</div>{/snippet}
|
||||
<div class="reference-preferences">
|
||||
<button class="a-btn ghost sm" onclick={toggleInspector}
|
||||
>{focusMode ? '退出专注' : '专注创作'} ⤢</button
|
||||
>
|
||||
<div class="runtime-setting">
|
||||
<label for="comfy-endpoint">使用哪台 ComfyUI</label><select
|
||||
class="a-select"
|
||||
@@ -1407,9 +1543,40 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 44px;
|
||||
height: var(--desk-head-height);
|
||||
margin: 0 calc(-1 * var(--desk-head-inset));
|
||||
padding: 0 var(--desk-head-inset);
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--desk-head-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@media (max-width: 1366px) {
|
||||
.workflow-file {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.history-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--info);
|
||||
border-radius: var(--r-pill);
|
||||
color: var(--info);
|
||||
font-size: 12.5px;
|
||||
background: color-mix(in srgb, var(--bg-2) 92%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: var(--sh-2);
|
||||
align-self: center;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.history-banner span {
|
||||
margin-right: auto;
|
||||
}
|
||||
.heading-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1420,22 +1587,71 @@
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--text-2);
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: var(--desk-head-name-size);
|
||||
font-weight: var(--desk-head-name-weight);
|
||||
line-height: var(--desk-head-name-line);
|
||||
max-width: 320px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.studio-heading :global(.a-btn) {
|
||||
min-height: 28px;
|
||||
font-size: 11px;
|
||||
padding: 4px 8px;
|
||||
.workflow-identity {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.fam-dot {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--desk-search-radius);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font: 700 9px var(--mono);
|
||||
color: var(--bg-1);
|
||||
background: var(--scene);
|
||||
}
|
||||
.workflow-file {
|
||||
font: 11px var(--mono);
|
||||
color: var(--tx-3);
|
||||
max-width: 260px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: auto;
|
||||
}
|
||||
.connection-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
height: 26px;
|
||||
padding: 0 10px;
|
||||
border-radius: var(--desk-connection-radius);
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg-2);
|
||||
color: var(--tx-2);
|
||||
white-space: nowrap;
|
||||
max-width: 260px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.connection-status i {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--tx-3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.connection-status i.connected {
|
||||
background: var(--ok);
|
||||
}
|
||||
.connection-status.failed i {
|
||||
background: var(--err);
|
||||
}
|
||||
.studio-layout {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr) 190px;
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -1462,7 +1678,7 @@
|
||||
gap: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: radial-gradient(ellipse at 50% 20%, #202028, #131316 70%);
|
||||
background: var(--bg-1);
|
||||
}
|
||||
.desk-welcome h1 {
|
||||
font-size: 24px;
|
||||
@@ -1542,12 +1758,9 @@
|
||||
}
|
||||
@media (max-height: 740px) {
|
||||
.studio-layout {
|
||||
grid-template-rows: minmax(0, 1fr) 172px;
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
}
|
||||
.refract-studio {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.refract-studio {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Same-tab fallback for browsers that deny session storage. No user data leaves the browser.
|
||||
const drafts = new Map<string, string>();
|
||||
export function readDeskDraft(key: string): string | null {
|
||||
try {
|
||||
return sessionStorage.getItem(key) ?? drafts.get(key) ?? null;
|
||||
} catch {
|
||||
return drafts.get(key) ?? null;
|
||||
}
|
||||
}
|
||||
export function writeDeskDraft(key: string, value: string) {
|
||||
drafts.set(key, value);
|
||||
try {
|
||||
sessionStorage.setItem(key, value);
|
||||
} catch {
|
||||
/* In-memory switching still works. */
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@
|
||||
}: Props = $props();
|
||||
|
||||
let mediaOpen = $state(false);
|
||||
let expanded = $state(false);
|
||||
let popoverInputs = $state<ComfyDynamicInput[]>([]);
|
||||
let popoverAnchor = $state<HTMLElement | null>(null);
|
||||
export function openMedia() {
|
||||
@@ -138,7 +139,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="composer-wrap" class:dock>
|
||||
<div class="composer-wrap" class:dock class:expanded>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={root}
|
||||
@@ -157,7 +158,10 @@
|
||||
<div class="composer-scroll" aria-label="创作输入">
|
||||
{@render header?.()}
|
||||
{#if current}
|
||||
<section class="input-section prompt-section">
|
||||
<section
|
||||
class="input-section prompt-section"
|
||||
class:has-slots={dock && hasSlots}
|
||||
>
|
||||
<h3>2 · 准备这次创作</h3>
|
||||
{#if hasSlots && !dock}<div class="slots">
|
||||
<SlotTray
|
||||
@@ -171,9 +175,9 @@
|
||||
{onFromResults}
|
||||
/>
|
||||
</div>{/if}
|
||||
{#if dock}<div class="media-summary">
|
||||
{#if dock}<div class="media-summary" class:has-slots={hasSlots}>
|
||||
{#if hasSlots}
|
||||
{#each ds.slots.slice(0, 4) as slot}<button
|
||||
{#each ds.slots.slice(0, 3) as slot}<button
|
||||
class="media-token"
|
||||
onclick={openMedia}
|
||||
aria-label={'设置' + slot.label}
|
||||
@@ -186,20 +190,36 @@
|
||||
</button>{/each}
|
||||
{#if ds.lists.length}<button
|
||||
class="media-token"
|
||||
onclick={openMedia}>+ 参考材料</button
|
||||
onclick={openMedia}
|
||||
>+ 参考材料 {ds.lists.reduce(
|
||||
(n, g) =>
|
||||
n +
|
||||
(Array.isArray(values[g.nodeId])
|
||||
? (values[g.nodeId] as string[]).length
|
||||
: 0),
|
||||
0,
|
||||
)}</button
|
||||
>{/if}
|
||||
<button class="media-token" onclick={openMedia}
|
||||
<button class="expand-prompt manage-media" onclick={openMedia}
|
||||
>{slotBusy ? '正在上传…' : '管理素材'}</button
|
||||
>
|
||||
{:else}<small>描述你的画面</small>{/if}
|
||||
<small class="input-shortcut">Ctrl + Enter 生成</small>
|
||||
{/if}
|
||||
</div>{/if}
|
||||
<div class="prompt-pair" class:dual={dock && !!ds.negative}>
|
||||
{#if ds.prompt}<div class="prompt-field">
|
||||
<label for="desk-prompt"
|
||||
>{ds.negative ? '正向提示词' : '提示词'}
|
||||
{#if ds.prompt.required}<span>必填</span>{/if}</label
|
||||
><textarea
|
||||
<div class="prompt-label">
|
||||
<label for="desk-prompt"
|
||||
>{ds.negative ? '正向提示词' : '提示词'}
|
||||
{#if ds.prompt.required}<span>必填</span>{/if}</label
|
||||
>
|
||||
<button
|
||||
class="expand-prompt"
|
||||
aria-expanded={expanded}
|
||||
onclick={() => (expanded = !expanded)}
|
||||
>{expanded ? '收起提示词' : '展开提示词'}</button
|
||||
>
|
||||
</div>
|
||||
<textarea
|
||||
bind:this={promptEl}
|
||||
id="desk-prompt"
|
||||
class="prompt"
|
||||
@@ -333,7 +353,7 @@
|
||||
disabled={!!block || submitting || slotBusy}
|
||||
onclick={onSubmit}
|
||||
>
|
||||
<Icon name="sparkle" size={16} />{submitting
|
||||
<Icon name="sparkle" size={14} />{submitting
|
||||
? '正在提交…'
|
||||
: slotBusy
|
||||
? '正在上传素材…'
|
||||
@@ -482,7 +502,7 @@
|
||||
background: var(--surface);
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
box-shadow: 0 -8px 20px #292a2d05;
|
||||
box-shadow: var(--sh-1);
|
||||
}
|
||||
.gen {
|
||||
width: 100%;
|
||||
@@ -520,24 +540,26 @@
|
||||
}
|
||||
|
||||
.dock {
|
||||
height: 100%;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
.dock .composer {
|
||||
height: 100%;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
border: 1px solid #f5b04246;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(125deg, #1b1a1d, #17161a);
|
||||
border: 1px solid var(--line-2);
|
||||
border-radius: var(--desk-dock-radius);
|
||||
background: var(--composer-bg);
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 0 0 1px #f5b04206;
|
||||
box-shadow: var(--desk-dock-shadow);
|
||||
}
|
||||
.dock .composer:focus-within {
|
||||
border-color: #f5b04280;
|
||||
border-color: color-mix(in srgb, var(--gold) 45%, transparent);
|
||||
box-shadow: var(--desk-dock-focus);
|
||||
}
|
||||
.dock .composer-scroll {
|
||||
padding: 9px 14px 4px;
|
||||
overflow: hidden;
|
||||
scrollbar-gutter: auto;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -557,50 +579,65 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 28px;
|
||||
margin-bottom: 6px;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.media-summary > small {
|
||||
color: var(--text-3);
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
|
||||
.dock .input-section.has-slots {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, var(--desk-slot-column)) minmax(0, 1fr);
|
||||
gap: 0;
|
||||
}
|
||||
.has-slots .media-summary {
|
||||
align-self: start;
|
||||
flex-wrap: wrap;
|
||||
margin: 0;
|
||||
padding: 10px 12px 8px;
|
||||
border-right: 1px solid var(--line);
|
||||
}
|
||||
.media-token {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: #ffffff04;
|
||||
border: 1px dashed #ffffff25;
|
||||
background: var(--bg-2);
|
||||
border: 1px dashed var(--line-3);
|
||||
border-radius: 6px;
|
||||
padding: 3px 7px;
|
||||
font-size: 10px;
|
||||
color: var(--text-2);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
height: 26px;
|
||||
height: var(--slot-size);
|
||||
}
|
||||
.media-token img {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
object-fit: cover;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.input-shortcut {
|
||||
.expand-prompt {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--tx-2);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dock .prompt-pair {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
gap: 18px;
|
||||
gap: 0;
|
||||
}
|
||||
.dock .prompt-pair.dual {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 0.65fr);
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
|
||||
}
|
||||
.dock .prompt-field {
|
||||
padding: 10px 14px 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
@@ -608,32 +645,41 @@
|
||||
}
|
||||
.dock .negative {
|
||||
border-left: 1px solid var(--line);
|
||||
padding-left: 18px;
|
||||
}
|
||||
.dock .prompt-field label {
|
||||
font-size: 10px;
|
||||
font-size: 11.5px;
|
||||
height: 20px;
|
||||
color: var(--text-3);
|
||||
margin: 0 0 4px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.prompt-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 20px;
|
||||
}
|
||||
.dock .prompt-field:first-child label {
|
||||
color: #d0b37d;
|
||||
color: var(--tx-2);
|
||||
}
|
||||
.dock .prompt {
|
||||
background: none;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
font-size: var(--fs-body);
|
||||
line-height: 1.6;
|
||||
resize: none;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
height: var(--prompt-height);
|
||||
flex: none;
|
||||
overflow: auto;
|
||||
}
|
||||
.dock.expanded .prompt {
|
||||
height: var(--prompt-expanded);
|
||||
}
|
||||
.dock .prompt:focus {
|
||||
outline: 0;
|
||||
}
|
||||
@@ -645,15 +691,18 @@
|
||||
margin-top: 3px;
|
||||
}
|
||||
.dock .prompt-counter {
|
||||
display: none;
|
||||
height: 22px;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.dock .submit {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 14px;
|
||||
height: 45px;
|
||||
padding: 8px 10px 10px 12px;
|
||||
height: 57px;
|
||||
box-sizing: border-box;
|
||||
background: none;
|
||||
border-top: 1px solid var(--line);
|
||||
@@ -668,12 +717,12 @@
|
||||
overflow: auto;
|
||||
}
|
||||
.parameter {
|
||||
background: #25242a;
|
||||
border: 1px solid #ffffff07;
|
||||
background: var(--bg-3);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 7px;
|
||||
padding: 5px 8px;
|
||||
font-size: 10px;
|
||||
color: #c4c2cb;
|
||||
color: var(--tx-2);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
max-width: 180px;
|
||||
@@ -682,20 +731,18 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.parameter:hover {
|
||||
border-color: #f5b04255;
|
||||
border-color: var(--line-3);
|
||||
}
|
||||
.dock .gen {
|
||||
width: auto;
|
||||
min-height: 32px;
|
||||
font-size: 11px;
|
||||
padding: 7px 18px;
|
||||
border-radius: 20px;
|
||||
width: var(--desk-gen-width);
|
||||
height: var(--desk-gen-height);
|
||||
min-height: var(--desk-gen-height);
|
||||
font-size: 14px;
|
||||
font-weight: var(--desk-gen-weight);
|
||||
padding: 0 16px;
|
||||
border-radius: var(--desk-gen-radius);
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(140deg, #ffc65e, #f5b042);
|
||||
color: #30210c;
|
||||
border-color: #edb458;
|
||||
box-shadow: 0 3px 12px #f5b04217;
|
||||
}
|
||||
.dock .why {
|
||||
position: absolute;
|
||||
@@ -704,14 +751,14 @@
|
||||
max-width: 65%;
|
||||
padding: 3px 8px;
|
||||
border-radius: 5px;
|
||||
background: #241e17;
|
||||
background: var(--bg-2);
|
||||
font-size: 10px;
|
||||
color: var(--warn);
|
||||
z-index: 1;
|
||||
}
|
||||
.dock .why.err {
|
||||
color: var(--err);
|
||||
background: #241717;
|
||||
background: var(--bg-2);
|
||||
}
|
||||
.dock .ready {
|
||||
display: none;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,218 +1,232 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { toStore } from 'svelte/store';
|
||||
import { projects } from '$lib/api';
|
||||
import { t } from '$lib/i18n.svelte';
|
||||
import Icon from '$lib/components/ui/Icon.svelte';
|
||||
import Modal from '$lib/components/ui/Modal.svelte';
|
||||
import type { IconName } from '$lib/components/ui/icons';
|
||||
import { NAV } from './nav';
|
||||
import { goto } from "$app/navigation";
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import { toStore } from "svelte/store";
|
||||
import { projects } from "$lib/api";
|
||||
import { t } from "$lib/i18n.svelte";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import Modal from "$lib/components/ui/Modal.svelte";
|
||||
import type { IconName } from "$lib/components/ui/icons";
|
||||
import { NAV } from "./nav";
|
||||
|
||||
/** Ctrl/⌘K 搜索:跳到任一页面或项目(↑↓ 选择,Enter 打开,Esc 关闭). */
|
||||
let { open = $bindable(false) }: { open?: boolean } = $props();
|
||||
/** Ctrl/⌘K 搜索:跳到任一页面或项目(↑↓ 选择,Enter 打开,Esc 关闭). */
|
||||
let { open = $bindable(false) }: { open?: boolean } = $props();
|
||||
|
||||
interface Entry {
|
||||
key: string;
|
||||
group: 'pages' | 'projects';
|
||||
label: string;
|
||||
icon: IconName;
|
||||
href: string;
|
||||
}
|
||||
interface Entry {
|
||||
key: string;
|
||||
group: "pages" | "projects";
|
||||
label: string;
|
||||
icon: IconName;
|
||||
href: string;
|
||||
}
|
||||
|
||||
const listId = `palette-${Math.random().toString(36).slice(2, 9)}`;
|
||||
let query = $state('');
|
||||
let activeIndex = $state(0);
|
||||
let list = $state<HTMLDivElement | null>(null);
|
||||
const listId = `palette-${Math.random().toString(36).slice(2, 9)}`;
|
||||
let query = $state("");
|
||||
let activeIndex = $state(0);
|
||||
let list = $state<HTMLDivElement | null>(null);
|
||||
|
||||
const projectsQuery = createQuery(
|
||||
toStore(() => ({
|
||||
queryKey: ['projects'],
|
||||
queryFn: projects.list,
|
||||
enabled: open,
|
||||
staleTime: 30_000,
|
||||
})),
|
||||
);
|
||||
const projectsQuery = createQuery(
|
||||
toStore(() => ({
|
||||
queryKey: ["projects"],
|
||||
queryFn: projects.list,
|
||||
enabled: open,
|
||||
staleTime: 30_000,
|
||||
})),
|
||||
);
|
||||
|
||||
const entries = $derived.by<Entry[]>(() => {
|
||||
const pages: Entry[] = NAV.map((item) => ({
|
||||
key: `page-${item.id}`,
|
||||
group: 'pages',
|
||||
label: t(item.labelKey),
|
||||
icon: item.icon,
|
||||
href: item.href,
|
||||
}));
|
||||
const projectEntries: Entry[] = ($projectsQuery.data ?? []).map((p) => ({
|
||||
key: `project-${p.id}`,
|
||||
group: 'projects',
|
||||
label: p.title,
|
||||
icon: 'story',
|
||||
href: `/workbench/${p.id}`,
|
||||
}));
|
||||
const q = query.trim().toLowerCase();
|
||||
const all = [...pages, ...projectEntries];
|
||||
return q ? all.filter((e) => e.label.toLowerCase().includes(q)) : all;
|
||||
});
|
||||
const entries = $derived.by<Entry[]>(() => {
|
||||
const pages: Entry[] = NAV.map((item) => ({
|
||||
key: `page-${item.href}`,
|
||||
group: "pages",
|
||||
label: item.label,
|
||||
icon: item.icon,
|
||||
href: item.href,
|
||||
}));
|
||||
const projectEntries: Entry[] = ($projectsQuery.data ?? []).map((p) => ({
|
||||
key: `project-${p.id}`,
|
||||
group: "projects",
|
||||
label: p.title,
|
||||
icon: "story",
|
||||
href: `/workbench/${p.id}`,
|
||||
}));
|
||||
const q = query.trim().toLowerCase();
|
||||
const all = [...pages, ...projectEntries];
|
||||
return q ? all.filter((e) => e.label.toLowerCase().includes(q)) : all;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
void query;
|
||||
activeIndex = 0;
|
||||
});
|
||||
$effect(() => {
|
||||
void query;
|
||||
activeIndex = 0;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!open) query = '';
|
||||
});
|
||||
$effect(() => {
|
||||
if (!open) query = "";
|
||||
});
|
||||
|
||||
function optionId(index: number) {
|
||||
return `${listId}-${index}`;
|
||||
}
|
||||
function optionId(index: number) {
|
||||
return `${listId}-${index}`;
|
||||
}
|
||||
|
||||
function go(entry: Entry) {
|
||||
open = false;
|
||||
void goto(entry.href);
|
||||
}
|
||||
function go(entry: Entry) {
|
||||
open = false;
|
||||
void goto(entry.href);
|
||||
}
|
||||
|
||||
function move(step: number) {
|
||||
if (entries.length === 0) return;
|
||||
activeIndex = (activeIndex + step + entries.length) % entries.length;
|
||||
list?.querySelector(`#${optionId(activeIndex)}`)?.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
function move(step: number) {
|
||||
if (entries.length === 0) return;
|
||||
activeIndex = (activeIndex + step + entries.length) % entries.length;
|
||||
list
|
||||
?.querySelector(`#${optionId(activeIndex)}`)
|
||||
?.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
move(1);
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
move(-1);
|
||||
} else if (event.key === 'Enter') {
|
||||
const entry = entries[activeIndex];
|
||||
if (entry) {
|
||||
event.preventDefault();
|
||||
go(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
move(1);
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
move(-1);
|
||||
} else if (event.key === "Enter") {
|
||||
const entry = entries[activeIndex];
|
||||
if (entry) {
|
||||
event.preventDefault();
|
||||
go(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function focusOnMount(node: HTMLInputElement) {
|
||||
requestAnimationFrame(() => node.focus());
|
||||
}
|
||||
function focusOnMount(node: HTMLInputElement) {
|
||||
requestAnimationFrame(() => node.focus());
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:open title={t('shell.searchTitle')}>
|
||||
<div class="palette">
|
||||
<div class="palette-search">
|
||||
<Icon name="search" size={16} />
|
||||
<input
|
||||
use:focusOnMount
|
||||
class="palette-input"
|
||||
type="text"
|
||||
bind:value={query}
|
||||
placeholder={t('shell.searchPlaceholder')}
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
aria-controls={listId}
|
||||
aria-autocomplete="list"
|
||||
aria-activedescendant={entries.length ? optionId(activeIndex) : undefined}
|
||||
aria-label={t('shell.searchTitle')}
|
||||
onkeydown={handleKeydown}
|
||||
/>
|
||||
</div>
|
||||
<div class="palette-list" id={listId} role="listbox" bind:this={list} aria-label={t('shell.searchTitle')}>
|
||||
{#if entries.length === 0}
|
||||
<p class="palette-empty">{t('shell.searchEmpty', { q: query.trim() })}</p>
|
||||
{/if}
|
||||
{#each entries as entry, i (entry.key)}
|
||||
{#if i === 0 || entries[i - 1].group !== entry.group}
|
||||
<div class="a-menu-group" role="presentation">
|
||||
{entry.group === 'pages' ? t('shell.searchGroupPages') : t('shell.searchGroupProjects')}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
id={optionId(i)}
|
||||
class="a-menu-item"
|
||||
class:active={i === activeIndex}
|
||||
role="option"
|
||||
tabindex="-1"
|
||||
aria-selected={i === activeIndex}
|
||||
onpointermove={() => (activeIndex = i)}
|
||||
onmousedown={(e) => e.preventDefault()}
|
||||
onclick={() => go(entry)}
|
||||
onkeydown={handleKeydown}
|
||||
>
|
||||
<Icon name={entry.icon} size={15} />
|
||||
<span class="a-menu-label">{entry.label}</span>
|
||||
<span class="a-menu-meta mono">{entry.href}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<p class="palette-hint">
|
||||
<span class="a-kbd">↑↓</span>{t('shell.searchHintMove')}
|
||||
<span class="a-kbd">Enter</span>{t('shell.searchHintOpen')}
|
||||
<span class="a-kbd">Esc</span>{t('shell.searchHintClose')}
|
||||
</p>
|
||||
</div>
|
||||
<Modal bind:open title={t("shell.searchTitle")}>
|
||||
<div class="palette">
|
||||
<div class="palette-search">
|
||||
<Icon name="search" size={16} />
|
||||
<input
|
||||
use:focusOnMount
|
||||
class="palette-input"
|
||||
type="text"
|
||||
bind:value={query}
|
||||
placeholder={t("shell.searchPlaceholder")}
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
aria-controls={listId}
|
||||
aria-autocomplete="list"
|
||||
aria-activedescendant={entries.length
|
||||
? optionId(activeIndex)
|
||||
: undefined}
|
||||
aria-label={t("shell.searchTitle")}
|
||||
onkeydown={handleKeydown}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="palette-list"
|
||||
id={listId}
|
||||
role="listbox"
|
||||
bind:this={list}
|
||||
aria-label={t("shell.searchTitle")}
|
||||
>
|
||||
{#if entries.length === 0}
|
||||
<p class="palette-empty">
|
||||
{t("shell.searchEmpty", { q: query.trim() })}
|
||||
</p>
|
||||
{/if}
|
||||
{#each entries as entry, i (entry.key)}
|
||||
{#if i === 0 || entries[i - 1].group !== entry.group}
|
||||
<div class="a-menu-group" role="presentation">
|
||||
{entry.group === "pages"
|
||||
? t("shell.searchGroupPages")
|
||||
: t("shell.searchGroupProjects")}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
id={optionId(i)}
|
||||
class="a-menu-item"
|
||||
class:active={i === activeIndex}
|
||||
role="option"
|
||||
tabindex="-1"
|
||||
aria-selected={i === activeIndex}
|
||||
onpointermove={() => (activeIndex = i)}
|
||||
onmousedown={(e) => e.preventDefault()}
|
||||
onclick={() => go(entry)}
|
||||
onkeydown={handleKeydown}
|
||||
>
|
||||
<Icon name={entry.icon} size={15} />
|
||||
<span class="a-menu-label">{entry.label}</span>
|
||||
<span class="a-menu-meta mono">{entry.href}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<p class="palette-hint">
|
||||
<span class="a-kbd">↑↓</span>{t("shell.searchHintMove")}
|
||||
<span class="a-kbd">Enter</span>{t("shell.searchHintOpen")}
|
||||
<span class="a-kbd">Esc</span>{t("shell.searchHintClose")}
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.palette {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
.palette-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
padding: 0 var(--sp-5);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--bg-3);
|
||||
color: var(--text-3);
|
||||
transition: border-color var(--d-state) ease;
|
||||
}
|
||||
.palette-search:focus-within {
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.palette-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 40px;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: var(--fs-section);
|
||||
}
|
||||
.palette-input::placeholder {
|
||||
color: var(--text-3);
|
||||
}
|
||||
.palette-list {
|
||||
max-height: min(360px, 50vh);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.palette-empty {
|
||||
margin: 0;
|
||||
padding: var(--sp-6) var(--sp-4);
|
||||
font-size: var(--fs-aux);
|
||||
color: var(--text-3);
|
||||
text-align: center;
|
||||
}
|
||||
.palette-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
margin: 0;
|
||||
font-size: var(--fs-mini);
|
||||
color: var(--text-3);
|
||||
}
|
||||
.palette-hint .a-kbd:not(:first-child) {
|
||||
margin-left: var(--sp-3);
|
||||
}
|
||||
@media (hover: none) {
|
||||
.palette-hint {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.palette {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
.palette-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
padding: 0 var(--sp-5);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--bg-3);
|
||||
color: var(--text-3);
|
||||
transition: border-color var(--d-state) ease;
|
||||
}
|
||||
.palette-search:focus-within {
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.palette-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 40px;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: var(--fs-section);
|
||||
}
|
||||
.palette-input::placeholder {
|
||||
color: var(--text-3);
|
||||
}
|
||||
.palette-list {
|
||||
max-height: min(360px, 50vh);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.palette-empty {
|
||||
margin: 0;
|
||||
padding: var(--sp-6) var(--sp-4);
|
||||
font-size: var(--fs-aux);
|
||||
color: var(--text-3);
|
||||
text-align: center;
|
||||
}
|
||||
.palette-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
margin: 0;
|
||||
font-size: var(--fs-mini);
|
||||
color: var(--text-3);
|
||||
}
|
||||
.palette-hint .a-kbd:not(:first-child) {
|
||||
margin-left: var(--sp-3);
|
||||
}
|
||||
@media (hover: none) {
|
||||
.palette-hint {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
<script lang="ts">
|
||||
import { THEMES, theme, setTheme, themeStorageUnavailable } from "$lib/theme";
|
||||
let { compact = false }: { compact?: boolean } = $props();
|
||||
function move(event: KeyboardEvent) {
|
||||
const steps: Record<string, number> = {
|
||||
ArrowRight: 1,
|
||||
ArrowDown: 1,
|
||||
ArrowLeft: -1,
|
||||
ArrowUp: -1,
|
||||
};
|
||||
const step = steps[event.key];
|
||||
if (!step && event.key !== "Home" && event.key !== "End") return;
|
||||
event.preventDefault();
|
||||
const group = (event.currentTarget as HTMLElement).closest(
|
||||
".choices",
|
||||
) as HTMLElement;
|
||||
const buttons = [...group.querySelectorAll<HTMLButtonElement>("button")];
|
||||
const current = buttons.indexOf(
|
||||
document.activeElement as HTMLButtonElement,
|
||||
);
|
||||
const next =
|
||||
event.key === "Home"
|
||||
? 0
|
||||
: event.key === "End"
|
||||
? buttons.length - 1
|
||||
: (current + step + buttons.length) % buttons.length;
|
||||
buttons[next]?.focus();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="choices" class:compact role="group" aria-label="外观主题">
|
||||
{#each THEMES as item}
|
||||
<button
|
||||
class="choice"
|
||||
class:selected={$theme === item.id}
|
||||
aria-pressed={$theme === item.id}
|
||||
onkeydown={move}
|
||||
onclick={() => setTheme(item.id)}
|
||||
>
|
||||
<span class="preview" data-theme={item.id} aria-hidden="true"
|
||||
><span class="rail"></span><span class="list"
|
||||
><i></i><i></i><i></i></span
|
||||
><span class="stage"><i></i><b></b></span></span
|
||||
>
|
||||
<span class="copy"
|
||||
><strong>{item.name}</strong><small>{item.english}</small><span
|
||||
>{item.description}</span
|
||||
></span
|
||||
>
|
||||
<span class="check" aria-hidden="true"
|
||||
>{$theme === item.id ? "✓" : ""}</span
|
||||
>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if $themeStorageUnavailable}<p class="notice" role="status">
|
||||
浏览器禁止本地存储:本次有效,其他操作不受影响
|
||||
</p>{/if}
|
||||
|
||||
<style>
|
||||
.choices {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.choices.compact {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.choice {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 18px;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--line-2);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--bg-2);
|
||||
color: var(--tx-1);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.choice:hover {
|
||||
border-color: var(--line-3);
|
||||
background: var(--bg-3);
|
||||
}
|
||||
.choice.selected {
|
||||
border-color: var(--gold);
|
||||
}
|
||||
.choice:focus-visible {
|
||||
outline: 2px solid var(--info);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.compact .choice {
|
||||
grid-template-columns: 76px minmax(0, 1fr) 18px;
|
||||
align-items: center;
|
||||
}
|
||||
.preview {
|
||||
grid-column: 1 / -1;
|
||||
height: 64px;
|
||||
border-radius: var(--r-sm);
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: 12px 22px 1fr;
|
||||
background: var(--bg-0);
|
||||
border: 1px solid var(--line-2);
|
||||
}
|
||||
.compact .preview {
|
||||
grid-column: auto;
|
||||
height: 48px;
|
||||
}
|
||||
.rail,
|
||||
.list {
|
||||
background: var(--bg-1);
|
||||
border-right: 1px solid var(--line);
|
||||
}
|
||||
.rail::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
margin: 6px auto;
|
||||
border-radius: 50%;
|
||||
background: var(--gold);
|
||||
}
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
padding: 6px 3px;
|
||||
}
|
||||
.list i {
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--line-3);
|
||||
}
|
||||
.list i:first-child {
|
||||
background: var(--sc-image);
|
||||
}
|
||||
.stage {
|
||||
padding: 5px 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.stage i {
|
||||
flex: 1;
|
||||
border-radius: 3px;
|
||||
background: var(--bg-3);
|
||||
border: 1px solid var(--line-2);
|
||||
}
|
||||
.stage b {
|
||||
height: 9px;
|
||||
width: 18px;
|
||||
align-self: end;
|
||||
border-radius: 2px;
|
||||
background: var(--grad-gold);
|
||||
}
|
||||
.copy {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
.copy strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
.copy small {
|
||||
font: 10px var(--mono);
|
||||
color: var(--tx-3);
|
||||
align-self: center;
|
||||
}
|
||||
.copy > span {
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
color: var(--tx-2);
|
||||
}
|
||||
.check {
|
||||
color: var(--gold);
|
||||
}
|
||||
.notice {
|
||||
font-size: 12px;
|
||||
color: var(--tx-2);
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
@media (max-width: 1366px) {
|
||||
.choices:not(.compact) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,123 +1,130 @@
|
||||
import type { IconName } from '$lib/components/ui/icons';
|
||||
import type { IconName } from "$lib/components/ui/icons";
|
||||
|
||||
// IDs are retained for the existing page contracts while B1–B4 replace layouts.
|
||||
export type NavId =
|
||||
| 'projects'
|
||||
| 'workbench'
|
||||
| 'image'
|
||||
| 'video'
|
||||
| 'tasks'
|
||||
| 'agents'
|
||||
| 'connections'
|
||||
| 'models'
|
||||
| 'library'
|
||||
| 'settings'
|
||||
| 'canvas'
|
||||
| 'workflows';
|
||||
|
||||
| "projects"
|
||||
| "workbench"
|
||||
| "image"
|
||||
| "video"
|
||||
| "tasks"
|
||||
| "agents"
|
||||
| "connections"
|
||||
| "models"
|
||||
| "library"
|
||||
| "settings"
|
||||
| "canvas"
|
||||
| "workflows";
|
||||
export interface NavItem {
|
||||
id: NavId;
|
||||
labelKey: string;
|
||||
icon: IconName;
|
||||
href: string;
|
||||
id: NavId;
|
||||
label: string;
|
||||
icon: IconName;
|
||||
href: string;
|
||||
scene: "project" | "image" | "agent" | "manage" | "asset";
|
||||
matches: (path: string) => boolean;
|
||||
}
|
||||
|
||||
// Order is fixed by spec-v2 §3: 项目 / 创作工作台 / 生图 / 生视频 / 生成任务 /
|
||||
// 智能体 / ComfyUI 连接 / 模型设置 / 设置. HEL-649 §二 puts the connection
|
||||
// and settings entries at the foot of the 60px rail.
|
||||
export const NAV_MAIN: NavItem[] = [
|
||||
{ id: 'projects', labelKey: 'shell.navProjects', icon: 'home', href: '/' },
|
||||
{
|
||||
id: 'workbench',
|
||||
labelKey: 'shell.navWorkbench',
|
||||
icon: 'story',
|
||||
href: '/workbench',
|
||||
},
|
||||
{ id: 'image', labelKey: 'shell.navImage', icon: 'image', href: '/image' },
|
||||
{ id: 'video', labelKey: 'shell.navVideo', icon: 'film', href: '/video' },
|
||||
{ id: 'tasks', labelKey: 'shell.navTasks', icon: 'queue', href: '/tasks' },
|
||||
{ id: 'canvas', labelKey: 'nav.canvas', icon: 'story', href: '/canvas' },
|
||||
{
|
||||
id: 'agents',
|
||||
labelKey: 'shell.navAgents',
|
||||
icon: 'sparkle',
|
||||
href: '/agents',
|
||||
},
|
||||
{
|
||||
id: "projects",
|
||||
label: "工作室",
|
||||
icon: "home",
|
||||
href: "/",
|
||||
scene: "project",
|
||||
matches: (p) => p === "/",
|
||||
},
|
||||
{
|
||||
id: "workbench",
|
||||
label: "项目",
|
||||
icon: "folder",
|
||||
href: "/workbench",
|
||||
scene: "project",
|
||||
matches: (p) => /^\/(workbench|project)(\/|$)/.test(p),
|
||||
},
|
||||
{
|
||||
id: "image",
|
||||
label: "创作台",
|
||||
icon: "image",
|
||||
href: "/create",
|
||||
scene: "image",
|
||||
matches: (p) =>
|
||||
["/create", "/image", "/video", "/playground", "/build-scene"].includes(
|
||||
p,
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "agents",
|
||||
label: "智能体",
|
||||
icon: "sparkle",
|
||||
href: "/agents",
|
||||
scene: "agent",
|
||||
matches: (p) => p === "/agents",
|
||||
},
|
||||
{
|
||||
id: "canvas",
|
||||
label: "画布",
|
||||
icon: "story",
|
||||
href: "/canvas",
|
||||
scene: "manage",
|
||||
matches: (p) => p.startsWith("/canvas"),
|
||||
},
|
||||
{
|
||||
id: "library",
|
||||
label: "素材库",
|
||||
icon: "assets",
|
||||
href: "/library",
|
||||
scene: "asset",
|
||||
matches: (p) => p === "/library",
|
||||
},
|
||||
];
|
||||
export const NAV_MANAGE: NavItem[] = [
|
||||
{
|
||||
id: "tasks",
|
||||
label: "任务",
|
||||
icon: "clock",
|
||||
href: "/tasks",
|
||||
scene: "manage",
|
||||
matches: (p) => p === "/tasks",
|
||||
},
|
||||
{
|
||||
id: "workflows",
|
||||
label: "工作流",
|
||||
icon: "playground",
|
||||
href: "/workflows",
|
||||
scene: "manage",
|
||||
matches: (p) => p === "/workflows",
|
||||
},
|
||||
{
|
||||
id: "connections",
|
||||
label: "模型连接",
|
||||
icon: "link",
|
||||
href: "/model-connections",
|
||||
scene: "manage",
|
||||
matches: (p) =>
|
||||
["/connections", "/models", "/model-connections"].includes(p),
|
||||
},
|
||||
];
|
||||
|
||||
export const NAV_FOOT: NavItem[] = [
|
||||
{
|
||||
id: 'connections',
|
||||
labelKey: 'shell.navConnections',
|
||||
icon: 'link',
|
||||
href: '/connections',
|
||||
},
|
||||
{
|
||||
id: 'models',
|
||||
labelKey: 'shell.navModels',
|
||||
icon: 'assets',
|
||||
href: '/models',
|
||||
},
|
||||
{
|
||||
id: 'workflows',
|
||||
labelKey: 'connections.wfTitle',
|
||||
icon: 'playground',
|
||||
href: '/workflows',
|
||||
},
|
||||
{
|
||||
id: 'library',
|
||||
labelKey: 'shell.navLibrary',
|
||||
icon: 'folder',
|
||||
href: '/library',
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
labelKey: 'shell.navSettings',
|
||||
icon: 'settings',
|
||||
href: '/settings',
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
label: "设置",
|
||||
icon: "settings",
|
||||
href: "/settings",
|
||||
scene: "manage",
|
||||
matches: (p) => p === "/settings",
|
||||
},
|
||||
];
|
||||
|
||||
export const NAV: NavItem[] = [...NAV_MAIN, ...NAV_FOOT];
|
||||
|
||||
export interface TabItem {
|
||||
id: 'projects' | 'workbench' | 'generate';
|
||||
labelKey: string;
|
||||
icon: IconName;
|
||||
href: string;
|
||||
/** Pages that light this tab up. */
|
||||
covers: NavId[];
|
||||
}
|
||||
|
||||
/** ≤760px bottom tab bar (§二): 项目 / 创作 / 生成 / 更多. */
|
||||
export const TABS: TabItem[] = [
|
||||
{
|
||||
id: 'projects',
|
||||
labelKey: 'shell.tabProjects',
|
||||
icon: 'home',
|
||||
href: '/',
|
||||
covers: ['projects'],
|
||||
},
|
||||
{
|
||||
id: 'workbench',
|
||||
labelKey: 'shell.tabCreate',
|
||||
icon: 'story',
|
||||
href: '/workbench',
|
||||
covers: ['workbench'],
|
||||
},
|
||||
{
|
||||
id: 'generate',
|
||||
labelKey: 'shell.tabGenerate',
|
||||
icon: 'image',
|
||||
href: '/image',
|
||||
covers: ['image', 'video'],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Every page a tab does not link to directly lives in the 更多 sheet. 生视频
|
||||
* is still its own route, so it is listed there too until the two
|
||||
* generation pages merge.
|
||||
*/
|
||||
export const MORE_ITEMS: NavItem[] = NAV.filter(
|
||||
(item) => !TABS.some((tab) => tab.href === item.href),
|
||||
);
|
||||
// Tools awaiting their replacement keep reachable entries in global search.
|
||||
export const NAV = [
|
||||
...NAV_MAIN,
|
||||
...NAV_MANAGE,
|
||||
...NAV_FOOT,
|
||||
{ id: "video", label: "视频创作", icon: "film", href: "/video" },
|
||||
{ id: "models", label: "大语言模型", icon: "assets", href: "/models" },
|
||||
{
|
||||
id: "connections",
|
||||
label: "ComfyUI 连接",
|
||||
icon: "link",
|
||||
href: "/connections",
|
||||
},
|
||||
{ id: "image", label: "试验台", icon: "playground", href: "/playground" },
|
||||
{ id: "video", label: "三维搭景", icon: "story", href: "/build-scene" },
|
||||
] as const;
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
onclose?: () => void;
|
||||
footer?: Snippet;
|
||||
children: Snippet;
|
||||
/** Open above a dock, retaining the shared dialog focus and keyboard behavior. */
|
||||
anchor?: HTMLElement | null;
|
||||
headerActions?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -22,6 +25,8 @@
|
||||
onclose,
|
||||
footer,
|
||||
children,
|
||||
anchor = null,
|
||||
headerActions,
|
||||
}: Props = $props();
|
||||
|
||||
const FOCUSABLE =
|
||||
@@ -30,6 +35,28 @@
|
||||
const titleId = `modal-title-${Math.random().toString(36).slice(2, 9)}`;
|
||||
|
||||
let panel = $state<HTMLDivElement | null>(null);
|
||||
let anchorBox = $state({ left: 0, bottom: 0, width: 620, available: 560 });
|
||||
$effect(() => {
|
||||
if (!open || !anchor) return;
|
||||
const source = anchor;
|
||||
const place = () => {
|
||||
const r = source.getBoundingClientRect();
|
||||
anchorBox = {
|
||||
left: r.left + 12,
|
||||
bottom: innerHeight - r.top + 8,
|
||||
width: r.width - 24,
|
||||
available: Math.max(100, r.top - 64),
|
||||
};
|
||||
};
|
||||
place();
|
||||
const observer = new ResizeObserver(place);
|
||||
observer.observe(source);
|
||||
window.addEventListener('resize', place);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener('resize', place);
|
||||
};
|
||||
});
|
||||
const layer = Symbol('dialog');
|
||||
|
||||
function requestClose() {
|
||||
@@ -100,10 +127,20 @@
|
||||
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div class="modal-backdrop" onclick={handleBackdropClick}>
|
||||
<div
|
||||
class="modal-backdrop"
|
||||
class:anchored={!!anchor}
|
||||
onclick={handleBackdropClick}
|
||||
>
|
||||
<div
|
||||
bind:this={panel}
|
||||
class="modal-panel size-{size}"
|
||||
style:left={anchor ? `${anchorBox.left}px` : undefined}
|
||||
style:bottom={anchor ? `${anchorBox.bottom}px` : undefined}
|
||||
style:width={anchor
|
||||
? `min(var(--desk-pop-width), ${anchorBox.width}px)`
|
||||
: undefined}
|
||||
style:--anchor-available={`${anchorBox.available}px`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={title ? titleId : undefined}
|
||||
@@ -115,6 +152,7 @@
|
||||
{#if title}
|
||||
<h2 class="modal-title" id={titleId}>{title}</h2>
|
||||
{/if}
|
||||
{@render headerActions?.()}
|
||||
{#if dismissible}
|
||||
<button
|
||||
type="button"
|
||||
@@ -168,6 +206,38 @@
|
||||
.modal-panel.size-xl {
|
||||
width: min(1180px, 100%);
|
||||
}
|
||||
.anchored {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
.anchored .modal-panel {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
height: var(--desk-pop-height);
|
||||
max-height: min(64vh, var(--anchor-available));
|
||||
border: 1px solid var(--line-2);
|
||||
border-radius: var(--desk-dock-radius);
|
||||
background: var(--desk-pop-bg);
|
||||
box-shadow: var(--desk-pop-shadow);
|
||||
backdrop-filter: blur(14px);
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
.anchored .modal-header {
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.anchored .modal-title {
|
||||
font-size: var(--fs-body);
|
||||
}
|
||||
.anchored .modal-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.anchored .modal-footer {
|
||||
padding: 10px 14px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -92,14 +92,14 @@ Status → color mapping (unknown strings fall back to muted and show the raw st
|
||||
|
||||
`import ProgressBar from '$lib/components/ui/ProgressBar.svelte';`
|
||||
|
||||
| Prop | Type | Default | Notes |
|
||||
| --------------- | -------------- | ------- | ----------------------------------------- |
|
||||
| `value` | `number` | `0` | clamped to 0–100 |
|
||||
| `indeterminate` | `boolean` | `false` | sliding accent gradient, no % readout |
|
||||
| `label` | `string` | — | text above the track, also the aria-label |
|
||||
| `size` | `'sm' \| 'md'` | `'md'` | sm = 4px, md = 6px track |
|
||||
| `tone` | `'accent' \| 'success' \| 'error'` | `'accent'` | done = success, failed = error (§5) |
|
||||
| `bare` | `boolean` | `false` | track only, no label / % row |
|
||||
| Prop | Type | Default | Notes |
|
||||
| --------------- | ---------------------------------- | ---------- | ----------------------------------------- |
|
||||
| `value` | `number` | `0` | clamped to 0–100 |
|
||||
| `indeterminate` | `boolean` | `false` | sliding accent gradient, no % readout |
|
||||
| `label` | `string` | — | text above the track, also the aria-label |
|
||||
| `size` | `'sm' \| 'md'` | `'md'` | sm = 4px, md = 6px track |
|
||||
| `tone` | `'accent' \| 'success' \| 'error'` | `'accent'` | done = success, failed = error (§5) |
|
||||
| `bare` | `boolean` | `false` | track only, no label / % row |
|
||||
|
||||
```svelte
|
||||
<ProgressBar value={job.progress} label={job.name} />
|
||||
@@ -126,14 +126,16 @@ Status → color mapping (unknown strings fall back to muted and show the raw st
|
||||
|
||||
`import Modal from '$lib/components/ui/Modal.svelte';`
|
||||
|
||||
| Prop | Type | Default | Notes |
|
||||
| ------------- | ------------ | -------- | --------------------------------------------------------------------------- |
|
||||
| `open` | `boolean` | `false` | **bindable** — use `bind:open` |
|
||||
| `title` | `string` | — | wired to `aria-labelledby`; falls back to `aria-label="Dialog"` |
|
||||
| `dismissible` | `boolean` | `true` | gates Esc, backdrop click, and the close button |
|
||||
| `onclose` | `() => void` | — | fires once when the modal closes (any cause, incl. external `open = false`) |
|
||||
| `footer` | `Snippet` | — | right-aligned action row |
|
||||
| `children` | `Snippet` | required | body content |
|
||||
| Prop | Type | Default | Notes |
|
||||
| --------------- | --------------------- | -------- | -------------------------------------------------------------------------------------------- |
|
||||
| `open` | `boolean` | `false` | **bindable** — use `bind:open` |
|
||||
| `title` | `string` | — | wired to `aria-labelledby`; falls back to `aria-label="Dialog"` |
|
||||
| `dismissible` | `boolean` | `true` | gates Esc, backdrop click, and the close button |
|
||||
| `onclose` | `() => void` | — | fires once when the modal closes (any cause, incl. external `open = false`) |
|
||||
| `anchor` | `HTMLElement \| null` | `null` | Optional dock anchor: opens above it, clamps to viewport, retains the dialog focus behavior. |
|
||||
| `headerActions` | `Snippet` | — | Optional controls beside the title (for example restore workflow defaults). |
|
||||
| `footer` | `Snippet` | — | right-aligned action row |
|
||||
| `children` | `Snippet` | required | body content |
|
||||
|
||||
Behavior: Esc and backdrop click close when dismissible; focus is trapped while
|
||||
open; initial focus goes to the first focusable element (or the panel); focus is
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { createMutation, useQueryClient } from '@tanstack/svelte-query';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import ConfirmDialog from '$lib/components/ui/ConfirmDialog.svelte';
|
||||
@@ -35,6 +36,18 @@
|
||||
|
||||
let { rows, endpoints }: Props = $props();
|
||||
|
||||
// A pending workflow on the creative desk leads to its actual mapping/test row.
|
||||
let deepLinkDone = false;
|
||||
$effect(() => {
|
||||
if (deepLinkDone || !rows.length) return;
|
||||
const id = Number(page.url.searchParams.get('wf'));
|
||||
const row = rows.find((r) => r.id === id);
|
||||
deepLinkDone = true;
|
||||
if (!row) return;
|
||||
search = row.name;
|
||||
if (rowNeedsOutputMap(row)) void toggleWfMapEdit(row);
|
||||
});
|
||||
|
||||
const client = useQueryClient();
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
/** Approved HEL-726 themes. Colours live only in static/themes.css. */
|
||||
export const THEMES = [
|
||||
{
|
||||
id: "night",
|
||||
name: "夜片场",
|
||||
english: "Nocturne",
|
||||
description: "墨黑与钨丝金 · 沉浸式创作",
|
||||
},
|
||||
{
|
||||
id: "day",
|
||||
name: "日光棚",
|
||||
english: "Daylight",
|
||||
description: "暖白纸面 · 白天办公与长时间审稿",
|
||||
},
|
||||
{
|
||||
id: "grade",
|
||||
name: "调色间",
|
||||
english: "Grading",
|
||||
description: "中性深灰 · 判断作品色彩",
|
||||
},
|
||||
{
|
||||
id: "bluehour",
|
||||
name: "蓝调时刻",
|
||||
english: "Blue Hour",
|
||||
description: "靛蓝与珊瑚 · 提案与灵感发散",
|
||||
},
|
||||
] as const;
|
||||
export type ThemeId = (typeof THEMES)[number]["id"];
|
||||
export const THEME_KEY = "aigc-theme";
|
||||
export const theme = writable<ThemeId>("night");
|
||||
export const themeStorageUnavailable = writable(false);
|
||||
|
||||
export function isTheme(value: unknown): value is ThemeId {
|
||||
return THEMES.some((item) => item.id === value);
|
||||
}
|
||||
|
||||
export function setTheme(id: ThemeId) {
|
||||
if (typeof document === "undefined" || !isTheme(id)) return;
|
||||
// Only the root attribute changes: editors, uploads and jobs stay mounted.
|
||||
document.documentElement.dataset.theme = id;
|
||||
theme.set(id);
|
||||
try {
|
||||
localStorage.setItem(THEME_KEY, id);
|
||||
themeStorageUnavailable.set(false);
|
||||
} catch {
|
||||
themeStorageUnavailable.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
export function initTheme() {
|
||||
const current = document.documentElement.dataset.theme;
|
||||
theme.set(isTheme(current) ? current : "night");
|
||||
try {
|
||||
localStorage.getItem(THEME_KEY);
|
||||
} catch {
|
||||
themeStorageUnavailable.set(true);
|
||||
}
|
||||
const sync = (event: StorageEvent) => {
|
||||
if (event.key !== THEME_KEY && event.key !== null) return;
|
||||
const id = isTheme(event.newValue) ? event.newValue : "night";
|
||||
document.documentElement.dataset.theme = id;
|
||||
theme.set(id);
|
||||
};
|
||||
window.addEventListener("storage", sync);
|
||||
return () => window.removeEventListener("storage", sync);
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
<script lang="ts">
|
||||
import './app.css';
|
||||
import { onMount } from 'svelte';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
|
||||
import ToastHost from '$lib/components/ToastHost.svelte';
|
||||
import { initReduceMotion } from '$lib/reduceMotion.svelte';
|
||||
import "./app.css";
|
||||
import { initTheme } from "$lib/theme";
|
||||
import { onMount } from "svelte";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/svelte-query";
|
||||
import ToastHost from "$lib/components/ToastHost.svelte";
|
||||
import { initReduceMotion } from "$lib/reduceMotion.svelte";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { refetchOnWindowFocus: false, retry: 1 },
|
||||
},
|
||||
});
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { refetchOnWindowFocus: false, retry: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
onMount(() => initReduceMotion());
|
||||
onMount(() => initReduceMotion());
|
||||
onMount(initTheme);
|
||||
</script>
|
||||
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<slot />
|
||||
<ToastHost />
|
||||
<slot />
|
||||
<ToastHost />
|
||||
</QueryClientProvider>
|
||||
|
||||
+667
-811
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import StudioDesk from '$lib/components/desk/StudioDesk.svelte';
|
||||
const kind = $derived(
|
||||
page.url.searchParams.get('kind') === 'video' ? 'video' : 'image',
|
||||
);
|
||||
</script>
|
||||
|
||||
{#key kind + page.url.search}<StudioDesk {kind} />{/key}
|
||||
@@ -0,0 +1 @@
|
||||
export const ssr = false;
|
||||
@@ -1,6 +1,12 @@
|
||||
<script lang="ts">
|
||||
import StudioDesk from '$lib/components/desk/StudioDesk.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
onMount(() => {
|
||||
const url = new URL(page.url);
|
||||
url.searchParams.set('kind', 'image');
|
||||
void goto('/create' + url.search + url.hash, { replaceState: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
{#key page.url.pathname + page.url.search}<StudioDesk kind="image" />{/key}
|
||||
<p>正在打开创作台…</p>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
export const ssr = false;
|
||||
export const prerender = false;
|
||||
// Both existing management pages stay usable until B2 combines their tabs.
|
||||
export function load({ url }: { url: URL }) {
|
||||
const destination =
|
||||
url.searchParams.get("tab") === "llm" ? "/models" : "/connections";
|
||||
redirect(307, destination + url.search + new URL(url.href).hash);
|
||||
}
|
||||
@@ -1,41 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { agentApi, canvasApi } from '$lib/api';
|
||||
import { t } from '$lib/i18n.svelte';
|
||||
|
||||
let failed = $state('');
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const sessions = await agentApi.listSessions();
|
||||
const sandbox = sessions.find((s) => s.project_id == null);
|
||||
const s = sandbox ?? (await agentApi.createSession({}));
|
||||
const graph = await canvasApi.ensureForSession(s.id);
|
||||
goto(`/canvas/${graph.canvas.id}?session=${s.id}`, { replaceState: true });
|
||||
} catch (err) {
|
||||
failed = err instanceof Error ? err.message : t('playground.openFailed');
|
||||
}
|
||||
import { page } from '$app/state';
|
||||
onMount(() => {
|
||||
void goto('/create' + page.url.search + page.url.hash, {
|
||||
replaceState: true,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if failed}
|
||||
<div class="redirect-error" role="alert">{failed}</div>
|
||||
{:else}
|
||||
<div class="redirecting">{t('playground.opening')}</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.redirecting,
|
||||
.redirect-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
.redirect-error {
|
||||
color: var(--err);
|
||||
}
|
||||
</style>
|
||||
<p>正在打开创作台…</p>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const prerender = false;
|
||||
export const ssr = false;
|
||||
|
||||
export const load: PageLoad = ({ params }) => {
|
||||
redirect(308, `/workbench/${params.id}`);
|
||||
export const load: PageLoad = ({ params, url }) => {
|
||||
redirect(
|
||||
308,
|
||||
`/workbench/${params.id}${url.search}${new URL(url.href).hash}`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
|
||||
export const ssr = false;
|
||||
export const prerender = false;
|
||||
|
||||
export function load() {
|
||||
redirect(308, '/');
|
||||
export function load({ url }: { url: URL }) {
|
||||
redirect(308, "/" + url.search + new URL(url.href).hash);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,12 @@
|
||||
<script lang="ts">
|
||||
import StudioDesk from '$lib/components/desk/StudioDesk.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
onMount(() => {
|
||||
const url = new URL(page.url);
|
||||
url.searchParams.set('kind', 'video');
|
||||
void goto('/create' + url.search + url.hash, { replaceState: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
{#key page.url.pathname + page.url.search}<StudioDesk kind="video" />{/key}
|
||||
<p>正在打开创作台…</p>
|
||||
|
||||
@@ -0,0 +1,609 @@
|
||||
/* HEL-726 approved themes. Loaded before the app for first-paint safety. */
|
||||
:root,
|
||||
[data-theme="night"] {
|
||||
color-scheme: dark;
|
||||
/* surfaces: ink black with a faint blue cast, never pure #000 */
|
||||
--bg-0: #07080c;
|
||||
--bg-1: #0c0e14;
|
||||
--bg-2: #12151d;
|
||||
--bg-3: #1a1e29;
|
||||
--bg-4: #232838;
|
||||
--line: rgb(var(--ink) / 0.06);
|
||||
--line-2: rgb(var(--ink) / 0.11);
|
||||
--line-3: rgb(var(--ink) / 0.18);
|
||||
/* text: warm paper white */
|
||||
--tx-1: #f3efe6;
|
||||
--tx-2: #aaa79e;
|
||||
--tx-3: #6f6d68;
|
||||
--tx-inv: #1a1206;
|
||||
/* key light: tungsten gold */
|
||||
--gold: #e9b04b;
|
||||
--gold-hi: #f8d68c;
|
||||
--gold-lo: #b8771f;
|
||||
--grad-gold: linear-gradient(135deg, #fbe1a1 0%, #efb54f 45%, #d98a26 100%);
|
||||
/* scene lights — each workspace has its own colour temperature */
|
||||
--sc-image: #3fd8c6;
|
||||
--sc-video: #ff8a3d;
|
||||
--sc-agent: #a879ff;
|
||||
--sc-project: #e9b04b;
|
||||
--sc-manage: #7f93b5;
|
||||
--scene: var(--gold);
|
||||
/* status */
|
||||
--ok: #43d39a;
|
||||
--warn: #f2b33d;
|
||||
--err: #ff5d6c;
|
||||
--info: #5aa9ff;
|
||||
/* type */
|
||||
--font:
|
||||
"PingFang SC", "HarmonyOS Sans SC", "Microsoft YaHei UI", "Microsoft YaHei",
|
||||
system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
|
||||
--fs-display: 28px;
|
||||
--fs-h1: 20px;
|
||||
--fs-h2: 16px;
|
||||
--fs-body: 13px;
|
||||
--fs-small: 12px;
|
||||
--fs-micro: 11px;
|
||||
--slot-size: 56px;
|
||||
--prompt-height: 60px;
|
||||
--desk-dock-radius: 14px;
|
||||
--desk-dock-bg: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--bg-3) 92%, transparent),
|
||||
color-mix(in srgb, var(--bg-2) 96%, transparent)
|
||||
);
|
||||
--desk-dock-shadow: var(--sh-2);
|
||||
--desk-dock-focus:
|
||||
var(--sh-2), 0 0 0 3px color-mix(in srgb, var(--gold) 8%, transparent),
|
||||
0 0 40px color-mix(in srgb, var(--gold) 8%, transparent);
|
||||
--desk-head-height: var(--head);
|
||||
--desk-head-inset: 16px;
|
||||
--desk-head-bg: var(--glass);
|
||||
--desk-head-name-size: 14px;
|
||||
--desk-head-name-weight: 600;
|
||||
--desk-head-name-line: 1.55;
|
||||
--desk-search-height: 34px;
|
||||
--desk-search-size: 13px;
|
||||
--desk-search-key-font: 500 10.5px/1 var(--mono);
|
||||
--desk-search-key-padding: 3px 5px;
|
||||
--desk-search-key-radius: 4px;
|
||||
--desk-search-radius: var(--r-sm);
|
||||
--desk-filter-height: 24px;
|
||||
--desk-filter-size: 11.5px;
|
||||
--desk-filter-radius: 999px;
|
||||
--desk-rail-title-height: 40px;
|
||||
--desk-rail-gap: 8px;
|
||||
--desk-connection-radius: var(--r-pill);
|
||||
--desk-switch-width: 26px;
|
||||
--desk-switch-height: 15px;
|
||||
--desk-switch-knob: 11px;
|
||||
--desk-action-radius: 5px;
|
||||
--desk-gen-width: 131px;
|
||||
--desk-gen-radius: 10px;
|
||||
--desk-gen-height: 38px;
|
||||
--desk-gen-weight: 700;
|
||||
--desk-seg-radius: 9px;
|
||||
--desk-seg-selected-radius: 7px;
|
||||
--desk-slot-column: 272px;
|
||||
--desk-pop-width: 620px;
|
||||
--desk-pop-height: 560px;
|
||||
--desk-pop-bg: color-mix(in srgb, var(--bg-2) 98%, transparent);
|
||||
--desk-pop-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
|
||||
--prompt-expanded: 28vh;
|
||||
/* space (4 base) */
|
||||
--s1: 4px;
|
||||
--s2: 8px;
|
||||
--s3: 12px;
|
||||
--s4: 16px;
|
||||
--s5: 20px;
|
||||
--s6: 24px;
|
||||
--s8: 32px;
|
||||
/* radius */
|
||||
--r-xs: 4px;
|
||||
--r-sm: 6px;
|
||||
--r-md: 10px;
|
||||
--r-lg: 14px;
|
||||
--r-xl: 20px;
|
||||
--r-pill: 999px;
|
||||
/* elevation */
|
||||
--sh-1: 0 1px 0 rgb(var(--ink) / 0.04) inset, 0 2px 8px rgba(0, 0, 0, 0.35);
|
||||
--sh-2: 0 1px 0 rgb(var(--ink) / 0.05) inset, 0 12px 32px rgba(0, 0, 0, 0.5);
|
||||
--sh-gold:
|
||||
0 6px 22px rgba(233, 160, 60, 0.35),
|
||||
0 0 0 1px rgba(255, 220, 150, 0.35) inset;
|
||||
/* motion */
|
||||
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--ease-io: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
--t-fast: 120ms;
|
||||
--t-base: 200ms;
|
||||
--t-slow: 320ms;
|
||||
--t-reveal: 700ms;
|
||||
/* layout */
|
||||
--rail: 68px;
|
||||
--list: 272px;
|
||||
--hist: 92px;
|
||||
--head: 48px;
|
||||
/* ---- theme-semantic tokens (v2 · 主题化) ---- */
|
||||
--ink: 255 255 255; /* overlay channel: hover tint, hairlines (white in dark, ink in light) */
|
||||
--hi: white; /* scene-coloured text is mixed toward this for contrast */
|
||||
--on-scene: #140a03; /* text on a solid scene/status colour */
|
||||
--on-ok: #06140e;
|
||||
--sc-asset: #ff86b0; /* 素材资产 helper colour (agent roster) */
|
||||
--glass: color-mix(in srgb, var(--bg-1) 70%, transparent);
|
||||
--scrim: rgba(4, 5, 8, 0.6);
|
||||
--well: var(--bg-0); /* inputs, segmented track */
|
||||
--media-shadow:
|
||||
0 30px 80px rgba(0, 0, 0, 0.6), 0 0 0 1px rgb(255 255 255 / 0.07);
|
||||
--sh-gold-hover:
|
||||
0 8px 28px rgba(233, 160, 60, 0.5), 0 0 0 1px rgba(255, 230, 170, 0.5) inset;
|
||||
--brand-grad: radial-gradient(
|
||||
circle at 30% 25%,
|
||||
#fff3cf 0,
|
||||
#efb54f 35%,
|
||||
#9b5a12 100%
|
||||
);
|
||||
--brand-glow: 0 0 24px rgba(233, 176, 75, 0.35);
|
||||
--brand-ink: #1b1206;
|
||||
--display-grad: linear-gradient(
|
||||
90deg,
|
||||
var(--gold-hi),
|
||||
var(--gold) 60%,
|
||||
#ff9b5a
|
||||
);
|
||||
--glow: 1; /* 1 = glows on (dark themes), 0 = off (light / neutral) */
|
||||
/* signature effects */
|
||||
--gen-bg: #0b0d12;
|
||||
--grain: rgba(255, 255, 255, 0.08);
|
||||
--beam: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
color-mix(in srgb, var(--scene) 30%, transparent),
|
||||
rgba(255, 255, 255, 0.25),
|
||||
transparent
|
||||
);
|
||||
--lattice: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='24' viewBox='0 0 14 24'%3E%3Cpath d='M7 0l7 4v8l-7 4-7-4V4zM7 16v8' fill='none' stroke='white' stroke-opacity='.16' stroke-width='.8'/%3E%3C/svg%3E");
|
||||
--scanline: rgb(var(--ink) / 0.012);
|
||||
--stage-tint: 7%;
|
||||
/* agent atmosphere */
|
||||
--aur-1: #7c4dff;
|
||||
--aur-2: #ff5fa8;
|
||||
--aur-3: #2fd5c5;
|
||||
--aur-op: 0.33;
|
||||
--agent-hero: linear-gradient(
|
||||
120deg,
|
||||
rgba(32, 20, 58, 0.9),
|
||||
rgba(14, 12, 26, 0.92) 55%,
|
||||
rgba(10, 22, 30, 0.9)
|
||||
);
|
||||
--agent-title: linear-gradient(90deg, #fff, #d9c6ff 50%, #ffb3d9);
|
||||
--orb-core: radial-gradient(
|
||||
circle at 35% 30%,
|
||||
#fff 0,
|
||||
#e3d4ff 12%,
|
||||
#a879ff 40%,
|
||||
#4b1fa8 75%,
|
||||
transparent 76%
|
||||
);
|
||||
--particle: 214, 190, 255;
|
||||
--fail-a: #1a1215;
|
||||
--fail-b: #140f12;
|
||||
}
|
||||
|
||||
[data-theme="day"] {
|
||||
color-scheme: light;
|
||||
--bg-0: #e9e4da;
|
||||
--bg-1: #f4f1ea;
|
||||
--bg-2: #fbf9f5;
|
||||
--bg-3: #ffffff;
|
||||
--bg-4: #efe9df;
|
||||
--ink: 38 30 18;
|
||||
--line: rgb(38 30 18 / 0.09);
|
||||
--line-2: rgb(38 30 18 / 0.15);
|
||||
--line-3: rgb(38 30 18 / 0.26);
|
||||
--tx-1: #1f1b15;
|
||||
--tx-2: #55504a;
|
||||
--tx-3: #736d65;
|
||||
--tx-inv: #ffffff;
|
||||
--gold: #a65a0c;
|
||||
--gold-hi: #8a4a06;
|
||||
--gold-lo: #c98a3a;
|
||||
--grad-gold: linear-gradient(135deg, #bd6512 0%, #a9580d 55%, #984d08 100%);
|
||||
--sc-image: #0b8378;
|
||||
--sc-video: #c2511a;
|
||||
--sc-agent: #6d45d0;
|
||||
--sc-project: #a65a0c;
|
||||
--sc-manage: #4c6286;
|
||||
--sc-asset: #b93a72;
|
||||
--ok: #11804f;
|
||||
--warn: #9a6400;
|
||||
--err: #c42b41;
|
||||
--info: #2263c2;
|
||||
--hi: #141109;
|
||||
--on-scene: #ffffff;
|
||||
--on-ok: #ffffff;
|
||||
--sh-1:
|
||||
0 1px 0 rgba(255, 255, 255, 0.7) inset, 0 1px 2px rgba(60, 44, 20, 0.1);
|
||||
--sh-2:
|
||||
0 1px 0 rgba(255, 255, 255, 0.8) inset, 0 12px 32px rgba(60, 44, 20, 0.14),
|
||||
0 2px 6px rgba(60, 44, 20, 0.06);
|
||||
--sh-gold:
|
||||
0 4px 14px rgba(166, 90, 12, 0.28), 0 0 0 1px rgba(120, 60, 0, 0.18) inset;
|
||||
--sh-gold-hover:
|
||||
0 6px 20px rgba(166, 90, 12, 0.38), 0 0 0 1px rgba(120, 60, 0, 0.24) inset;
|
||||
--glass: color-mix(in srgb, var(--bg-1) 88%, transparent);
|
||||
--scrim: rgba(40, 32, 20, 0.34);
|
||||
--well: #ffffff;
|
||||
--media-shadow:
|
||||
0 14px 36px rgba(50, 38, 18, 0.2), 0 0 0 1px rgba(38, 30, 18, 0.1);
|
||||
--brand-grad: linear-gradient(145deg, #2c261d, #14110c);
|
||||
--brand-glow: 0 2px 8px rgba(40, 30, 15, 0.25);
|
||||
--brand-ink: #f2c46d;
|
||||
--display-grad: linear-gradient(90deg, #a65a0c, #c2511a);
|
||||
--glow: 0;
|
||||
--gen-bg: #e3ddd1;
|
||||
--grain: rgba(40, 30, 18, 0.1);
|
||||
--beam: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
color-mix(in srgb, var(--scene) 24%, transparent),
|
||||
rgba(255, 255, 255, 0.85),
|
||||
transparent
|
||||
);
|
||||
--lattice: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='24' viewBox='0 0 14 24'%3E%3Cpath d='M7 0l7 4v8l-7 4-7-4V4zM7 16v8' fill='none' stroke='%23261e12' stroke-opacity='.16' stroke-width='.8'/%3E%3C/svg%3E");
|
||||
--scanline: transparent;
|
||||
--stage-tint: 6%;
|
||||
--aur-1: #b9a0ff;
|
||||
--aur-2: #ffb39a;
|
||||
--aur-3: #8fe0d4;
|
||||
--aur-op: 0.42;
|
||||
--agent-hero: linear-gradient(120deg, #efe7ff, #faf7f1 55%, #e4f3ef);
|
||||
--agent-title: linear-gradient(90deg, #24163f, #5b34c4 55%, #a2346f);
|
||||
--orb-core: radial-gradient(
|
||||
circle at 35% 30%,
|
||||
#fff 0,
|
||||
#efe6ff 14%,
|
||||
#a98cf0 42%,
|
||||
#6d45d0 75%,
|
||||
transparent 76%
|
||||
);
|
||||
--particle: 109, 69, 208;
|
||||
--fail-a: #f6e3e3;
|
||||
--fail-b: #efd6d7;
|
||||
}
|
||||
|
||||
[data-theme="grade"] {
|
||||
--desk-filter-radius: 3px;
|
||||
--desk-connection-radius: 3px;
|
||||
color-scheme: dark;
|
||||
--bg-0: #1b1b1c;
|
||||
--bg-1: #222223;
|
||||
--bg-2: #29292b;
|
||||
--bg-3: #323234;
|
||||
--bg-4: #3c3c3f;
|
||||
--ink: 255 255 255;
|
||||
--line: rgb(255 255 255 / 0.075);
|
||||
--line-2: rgb(255 255 255 / 0.13);
|
||||
--line-3: rgb(255 255 255 / 0.22);
|
||||
--tx-1: #ecebe8;
|
||||
--tx-2: #acaba6;
|
||||
--tx-3: #85847f;
|
||||
--tx-inv: #141516;
|
||||
--r-xs: 2px;
|
||||
--r-sm: 4px;
|
||||
--r-md: 6px;
|
||||
--r-lg: 8px;
|
||||
--desk-dock-radius: 8px;
|
||||
--desk-gen-radius: 4px;
|
||||
--desk-seg-radius: 4px;
|
||||
--desk-seg-selected-radius: 4px;
|
||||
--r-xl: 10px;
|
||||
--gold: #d9dde2;
|
||||
--gold-hi: #f2f4f6;
|
||||
--gold-lo: #9aa1aa;
|
||||
--grad-gold: linear-gradient(180deg, #f6f7f8 0%, #d5d9de 100%);
|
||||
--sc-image: #6fc2b7;
|
||||
--sc-video: #d8a06c;
|
||||
--sc-agent: #a598d8;
|
||||
--sc-project: #d4bc8a;
|
||||
--sc-manage: #909cae;
|
||||
--sc-asset: #d394b0;
|
||||
--ok: #4cc38f;
|
||||
--warn: #e3a93b;
|
||||
--err: #ee5d70;
|
||||
--info: #5d9fe3;
|
||||
--hi: white;
|
||||
--on-scene: #141516;
|
||||
--on-ok: #06140e;
|
||||
--sh-1:
|
||||
0 1px 0 rgba(255, 255, 255, 0.035) inset, 0 2px 6px rgba(0, 0, 0, 0.35);
|
||||
--sh-2:
|
||||
0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 12px 30px rgba(0, 0, 0, 0.45);
|
||||
--sh-gold:
|
||||
0 4px 14px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.55) inset;
|
||||
--sh-gold-hover:
|
||||
0 6px 18px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.7) inset;
|
||||
--glass: color-mix(in srgb, var(--bg-1) 82%, transparent);
|
||||
--scrim: rgba(0, 0, 0, 0.55);
|
||||
--well: #18181a;
|
||||
--media-shadow:
|
||||
0 18px 44px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.07);
|
||||
--brand-grad: linear-gradient(145deg, #f1f2f3, #aeb3ba);
|
||||
--brand-glow: none;
|
||||
--brand-ink: #17181a;
|
||||
--display-grad: linear-gradient(90deg, #ffffff, #bfc5cc);
|
||||
--glow: 0;
|
||||
--gen-bg: #1a1b1d;
|
||||
--grain: rgba(255, 255, 255, 0.06);
|
||||
--beam: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.07),
|
||||
rgba(255, 255, 255, 0.22),
|
||||
transparent
|
||||
);
|
||||
--scanline: transparent;
|
||||
--stage-tint: 0%;
|
||||
--aur-1: #5d5a80;
|
||||
--aur-2: #7a6070;
|
||||
--aur-3: #4f7470;
|
||||
--aur-op: 0.16;
|
||||
--agent-hero: linear-gradient(120deg, #202027, #1b1c1e 60%, #1b2020);
|
||||
--agent-title: linear-gradient(90deg, #ffffff, #d4d0ea);
|
||||
--orb-core: radial-gradient(
|
||||
circle at 35% 30%,
|
||||
#fff 0,
|
||||
#e7e5f2 14%,
|
||||
#a598d8 44%,
|
||||
#3e3a5c 76%,
|
||||
transparent 77%
|
||||
);
|
||||
--particle: 205, 205, 215;
|
||||
--fail-a: #211a1b;
|
||||
--fail-b: #1b1617;
|
||||
}
|
||||
|
||||
[data-theme="bluehour"] {
|
||||
--desk-head-bg: color-mix(in srgb, #0f1838 55%, transparent);
|
||||
color-scheme: dark;
|
||||
--bg-0: #0a1029;
|
||||
--bg-1: #0f1838;
|
||||
--bg-2: #162148;
|
||||
--bg-3: #1f2c5c;
|
||||
--bg-4: #2a386e;
|
||||
--r-sm: 8px;
|
||||
--r-md: 12px;
|
||||
--r-lg: 18px;
|
||||
--desk-dock-radius: 18px;
|
||||
--desk-gen-radius: 999px;
|
||||
--desk-gen-width: 140.7px;
|
||||
--desk-action-radius: 8px;
|
||||
--desk-dock-bg: linear-gradient(
|
||||
180deg,
|
||||
rgba(31, 44, 92, 0.82),
|
||||
rgba(22, 33, 72, 0.9)
|
||||
);
|
||||
--r-xl: 26px;
|
||||
--ink: 214 224 255;
|
||||
--line: rgb(214 224 255 / 0.07);
|
||||
--line-2: rgb(214 224 255 / 0.12);
|
||||
--line-3: rgb(214 224 255 / 0.2);
|
||||
--tx-1: #eef1fb;
|
||||
--tx-2: #a9b1cc;
|
||||
--tx-3: #7680a2;
|
||||
--tx-inv: #2b0c05;
|
||||
--gold: #ff9a6c;
|
||||
--gold-hi: #ffc7a8;
|
||||
--gold-lo: #d9603f;
|
||||
--grad-gold: linear-gradient(135deg, #ffd3b4 0%, #ff9a6c 45%, #f0644f 100%);
|
||||
--sc-image: #4fd6ea;
|
||||
--sc-video: #ffbb55;
|
||||
--sc-agent: #b592ff;
|
||||
--sc-project: #ff9a6c;
|
||||
--sc-manage: #8aa0dc;
|
||||
--sc-asset: #ff86c0;
|
||||
--ok: #4fd69c;
|
||||
--warn: #ffc94d;
|
||||
--err: #ff4d6d;
|
||||
--info: #69b4ff;
|
||||
--hi: white;
|
||||
--on-scene: #1a0b04;
|
||||
--on-ok: #05150e;
|
||||
--sh-1:
|
||||
0 1px 0 rgba(214, 224, 255, 0.05) inset, 0 2px 8px rgba(0, 0, 10, 0.4);
|
||||
--sh-2:
|
||||
0 1px 0 rgba(214, 224, 255, 0.06) inset, 0 12px 32px rgba(0, 0, 12, 0.55);
|
||||
--sh-gold:
|
||||
0 6px 24px rgba(255, 120, 90, 0.38),
|
||||
0 0 0 1px rgba(255, 214, 186, 0.42) inset;
|
||||
--sh-gold-hover:
|
||||
0 8px 30px rgba(255, 120, 90, 0.52),
|
||||
0 0 0 1px rgba(255, 226, 206, 0.55) inset;
|
||||
--scrim: rgba(4, 6, 16, 0.62);
|
||||
--media-shadow:
|
||||
0 30px 80px rgba(0, 0, 14, 0.62), 0 0 0 1px rgba(214, 224, 255, 0.08);
|
||||
--brand-grad: radial-gradient(
|
||||
circle at 30% 25%,
|
||||
#ffe4d2 0,
|
||||
#ff9a6c 38%,
|
||||
#a3363a 100%
|
||||
);
|
||||
--brand-glow: 0 0 26px rgba(255, 140, 100, 0.42);
|
||||
--brand-ink: #2b0c05;
|
||||
--display-grad: linear-gradient(90deg, #ffd3b4, #ff9a6c 55%, #ff7096);
|
||||
--glow: 1;
|
||||
--gen-bg: #0b1124;
|
||||
--grain: rgba(214, 224, 255, 0.08);
|
||||
--beam: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
color-mix(in srgb, var(--scene) 34%, transparent),
|
||||
rgba(255, 236, 220, 0.3),
|
||||
transparent
|
||||
);
|
||||
--stage-tint: 10%;
|
||||
--aur-1: #3e5bff;
|
||||
--aur-2: #ff7a8a;
|
||||
--aur-3: #2fd5e5;
|
||||
--aur-op: 0.36;
|
||||
--agent-hero: linear-gradient(
|
||||
120deg,
|
||||
rgba(26, 32, 84, 0.92),
|
||||
rgba(12, 16, 40, 0.94) 55%,
|
||||
rgba(44, 18, 52, 0.9)
|
||||
);
|
||||
--agent-title: linear-gradient(90deg, #ffffff, #c9d4ff 50%, #ffb8a0);
|
||||
--orb-core: radial-gradient(
|
||||
circle at 35% 30%,
|
||||
#fff 0,
|
||||
#ffe1cf 12%,
|
||||
#ff9a6c 38%,
|
||||
#5a2fb0 76%,
|
||||
transparent 77%
|
||||
);
|
||||
--particle: 255, 206, 186;
|
||||
--fail-a: #1d1220;
|
||||
--fail-b: #160e1a;
|
||||
}
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
:root {
|
||||
--list: 248px;
|
||||
--hist: 84px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1280px) {
|
||||
:root {
|
||||
--rail: 60px;
|
||||
--list: 236px;
|
||||
--hist: 76px;
|
||||
--head: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Compatibility names for existing pages; no independent colour palette. */
|
||||
:root {
|
||||
--accent-grad-hover: var(--grad-gold);
|
||||
--accent-ring: color-mix(in srgb, var(--gold) 25%, transparent);
|
||||
--accent-wash: var(--accent-soft);
|
||||
--btn-glow-hover: var(--sh-gold-hover);
|
||||
--composer-bg: var(--desk-dock-bg);
|
||||
--composer-focus: var(--bg-2);
|
||||
--float-bg: var(--bg-2);
|
||||
--on-media: #fff;
|
||||
--on-media-line: #ffffff66;
|
||||
--sel-shadow: 0 0 0 1px var(--gold);
|
||||
--bg: var(--bg-0);
|
||||
--paper: var(--bg-2);
|
||||
--lime: color-mix(in srgb, var(--sc-image) 14%, var(--bg-2));
|
||||
--rose: color-mix(in srgb, var(--sc-asset) 14%, var(--bg-2));
|
||||
--text: var(--tx-1);
|
||||
--text-2: var(--tx-2);
|
||||
--text-3: var(--tx-3);
|
||||
--accent: var(--gold);
|
||||
--accent-hi: var(--gold-hi);
|
||||
--accent-lo: var(--gold-lo);
|
||||
--accent-ink: var(--tx-inv);
|
||||
--accent-soft: color-mix(in srgb, var(--gold) 9%, transparent);
|
||||
--accent-line: color-mix(in srgb, var(--gold) 35%, transparent);
|
||||
--accent-grad: var(--grad-gold);
|
||||
--accent-glow: var(--sh-gold);
|
||||
--btn-glow: var(--sh-gold);
|
||||
--page-glow: linear-gradient(transparent, transparent);
|
||||
--ok-soft: color-mix(in srgb, var(--ok) 9%, transparent);
|
||||
--run: var(--info);
|
||||
--run-soft: color-mix(in srgb, var(--info) 9%, transparent);
|
||||
--run-grad: linear-gradient(90deg, var(--info), var(--sc-agent));
|
||||
--err-solid: var(--err);
|
||||
--err-soft: color-mix(in srgb, var(--err) 9%, transparent);
|
||||
--err-line: color-mix(in srgb, var(--err) 40%, transparent);
|
||||
--warn-soft: color-mix(in srgb, var(--warn) 9%, transparent);
|
||||
--overlay: var(--scrim);
|
||||
--media-scrim: #101017ee;
|
||||
--media-text: #fff;
|
||||
--media-line: #ffffff66;
|
||||
--stage-bg: var(--bg-0);
|
||||
--fs-title: 15px;
|
||||
--fs-section: 14px;
|
||||
--fs-body-sm: 12.5px;
|
||||
--fs-aux: 12px;
|
||||
--fs-mini: 11px;
|
||||
--lh-body: 1.55;
|
||||
--sp-1: 4px;
|
||||
--sp-2: 6px;
|
||||
--sp-3: 8px;
|
||||
--sp-4: 10px;
|
||||
--sp-5: 12px;
|
||||
--sp-6: 16px;
|
||||
--sp-7: 20px;
|
||||
--sp-8: 28px;
|
||||
--shadow-float: var(--sh-2);
|
||||
--rail-w: var(--rail);
|
||||
--topbar-h: var(--head);
|
||||
--tabbar-h: 0px;
|
||||
--sprocket: none;
|
||||
--frame-w: 0px;
|
||||
--frame-arm: 12px;
|
||||
--frame-inset: -4px;
|
||||
--paper-w: 4px;
|
||||
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
||||
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
|
||||
--d-press: 160ms;
|
||||
--d-state: 160ms;
|
||||
--d-pop: 180ms;
|
||||
--d-leave: 200ms;
|
||||
--d-enter: 220ms;
|
||||
--d-drawer: 250ms;
|
||||
--d-flash: 900ms;
|
||||
--d-progress: 400ms;
|
||||
--press-scale: 0.97;
|
||||
--avatar-1: color-mix(in srgb, var(--sc-project) 20%, var(--bg-2));
|
||||
--avatar-2: color-mix(in srgb, var(--sc-image) 20%, var(--bg-2));
|
||||
--avatar-3: color-mix(in srgb, var(--sc-video) 20%, var(--bg-2));
|
||||
--avatar-4: color-mix(in srgb, var(--sc-asset) 20%, var(--bg-2));
|
||||
--avatar-5: color-mix(in srgb, var(--sc-agent) 20%, var(--bg-2));
|
||||
--id-1: var(--sc-project);
|
||||
--id-2: var(--sc-image);
|
||||
--id-3: var(--sc-video);
|
||||
--id-4: var(--sc-asset);
|
||||
--id-5: var(--sc-agent);
|
||||
--surface: var(--bg-2);
|
||||
--surface-2: var(--bg-3);
|
||||
--surface-3: var(--bg-4);
|
||||
--border: var(--line);
|
||||
--border-strong: var(--line-2);
|
||||
--border-hover: var(--line-2);
|
||||
--accent-dim: var(--accent-soft);
|
||||
--success: var(--ok);
|
||||
--success-dim: var(--ok-soft);
|
||||
--info-dim: color-mix(in srgb, var(--info) 9%, transparent);
|
||||
--error: var(--err);
|
||||
--error-dim: var(--err-soft);
|
||||
--warning: var(--warn);
|
||||
--bg-primary: var(--bg);
|
||||
--bg-surface: var(--bg-2);
|
||||
--bg-elevated: var(--bg-3);
|
||||
--text-primary: var(--text);
|
||||
--text-secondary: var(--text-2);
|
||||
--text-muted: var(--text-3);
|
||||
--font-a-mono: var(--mono);
|
||||
--font-display: var(--font);
|
||||
--font-body: var(--font);
|
||||
--font-mono: var(--mono);
|
||||
--radius-sm: var(--r-sm);
|
||||
--radius-md: var(--r-md);
|
||||
--radius-lg: var(--r-lg);
|
||||
--space-sm: var(--sp-3);
|
||||
--space-md: var(--sp-6);
|
||||
--pill-bg: var(--bg-3);
|
||||
--pill-border: var(--line-2);
|
||||
--pill-radius: var(--r-pill);
|
||||
--pill-height: 36px;
|
||||
--pill-padding: 0 14px;
|
||||
--pill-font-size: var(--fs-body);
|
||||
}
|
||||
|
||||
html {
|
||||
background: var(--bg-0);
|
||||
color: var(--tx-1);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
# 夜片场 Nocturne Set — AIGC 工作台设计规范 v1(待白栖知确认)
|
||||
|
||||
> 视觉总监出具。本文件和 `index.html`(交互样本)、`shots/*.png` 一起,是施工和验收的唯一品味依据。样本中的画面都是程序生成的示意图,不是真实生成结果,也不调用任何接口。
|
||||
|
||||
## 0. 一句话方向
|
||||
|
||||
**深夜片场:墨黑的棚里只亮一盏钨丝灯,作品是唯一被照亮的东西。**
|
||||
本项目本质是“从故事到成片”的制片工具,所以主题元素取自片场,而不是暗房或印刷:取景框角标(选中)、扫描光束(生成中)、显影(结果到达)、场记编号与时间码(等宽数字)、每个工作区一种“场景色温”。参考站学的是它的骨架:紧凑、左侧双栏、底部输入座、参数胶囊、按场景变化的主题色;没有照抄它的配色。
|
||||
|
||||
## 1. 事实盘点(已核实)
|
||||
|
||||
| 项 | 事实 | 来源 |
|
||||
|---|---|---|
|
||||
| 前端路由 | 项目首页 `/`、工作台 `/workbench/[id]`、生图 `/image`、生视频 `/video`、任务 `/tasks`、智能体 `/agents`、画布 `/canvas/[id]`、素材库 `/library`、ComfyUI 连接 `/connections`、模型设置 `/models`、设置 `/settings`、三维搭景 `/build-scene`、试验台 `/playground` | 仓库 `calliope-web/src/routes` |
|
||||
| 五阶段 | 剧本 → 视觉 → 分镜 → 素材 → 镜头脚本;每阶段一个智能体,草稿需人工确认后才交给下一阶段,可恢复已确认版 | `i18n/zh.ts` workbench.* |
|
||||
| 工作流文件 | 仓库 `example_ComfyUI_workflows` 共 **156** 个文件:**10** 个是 API 格式,**146** 个是 ComfyUI 界面格式(需重新导出 API 格式并做输入映射才能执行) | 逐个文件检查 `nodes`/`last_node_id` 字段 |
|
||||
| 已可执行 | 测试站 `/api/workflows` 当前 **9** 条(图片 5、视频 4) | 测试站接口,只读 |
|
||||
| 素材槽 | 参考图(1–N)、遮罩、首帧、尾帧(可选)、参考视频、参考音频;按工作流声明出现 | studio.* 文案 + 组件 |
|
||||
| 模型设置 | 当前仅开放 LLM;图片/视频模型标“后续开放” | models.* |
|
||||
| 历次投诉 | 结果被挤小、历史穿插在生成区、大片空白、窗口不全、生成按钮要滚动才看到、视频自动播放且空格乱切、找不到播放键、百余工作流难找、智能体只做成了聊天 | 主卡 10/13–10/14 评论 |
|
||||
|
||||
## 2. 页面分工与核心路径
|
||||
|
||||
**用户要做的三件事**,决定了三个主场;其余页面都是它们的仓库或后台。
|
||||
|
||||
| 用户要做什么 | 从哪里进入 | 连续操作 | 看到/处理结果 | 回到项目 |
|
||||
|---|---|---|---|---|
|
||||
| A. 推进一个片子 | 工作室首页「继续创作」大卡 / 项目卡 | 项目页顶部五阶段 → 当前阶段的内容网格 → 右侧本阶段助手(补充要求/重新生成) | 草稿条提示“待确认”,底栏固定「确认,进入下一阶段」 | 本身就在项目里 |
|
||||
| B. 生成一张图/一段视频 | 侧栏「创作台」或项目镜头卡「去生视频」 | 左列找工作流 → 底座放素材、写正反提示词 → 参数胶囊 → 生成 | 中间舞台整幅显示;右侧历史竖条单独存在,点开时顶部提示“正在查看历史”,可复用或回到最新 | 结果下方「存入项目」→ 选项目与阶段,进入“待确认” |
|
||||
| C. 让助手干活 | 侧栏「智能体」或项目阶段的「对话」 | 首页大输入框写想法、绑定项目/模型/技能 → 任务页看计划、工具调用 | 右侧成果栏是可编辑草稿(分镜表/角色/提示词),单镜可重画 | 「带回项目待确认」→ 项目页出现紫色草稿条 |
|
||||
|
||||
**导航(左侧 68px 总导航,图标+文字常显)**:工作室、项目、创作台、智能体、画布、素材库 | 任务、工作流、模型连接 | 底部:队列、动效开关、设置。
|
||||
- 双左栏只在创作台出现(总导航 68 + 工作流列 272),两栏分别回答“去哪个功能”和“用哪个工作流”,宽度差一倍,不会混淆。
|
||||
- 原「生图」「生视频」两个路由合并为创作台,用顶部分段切换;切换时各自记住工作流、参数和输入。
|
||||
- 原「ComfyUI 连接」与「模型设置」合为「模型连接」,四个页签:ComfyUI 连接 / 大语言模型 / 图片模型 / 视频模型。
|
||||
|
||||
**设计判断(为什么这样分)**
|
||||
1. 创作台舞台是“作品优先”:不放详情、不放历史,结果按真实比例 `contain` 适配剩余空间;操作条固定在作品正下方,不覆盖画面。
|
||||
2. 历史是独立竖条(92px),只承担“回看”;回看是显式状态(蓝色提示条),绝不替换当前输入。
|
||||
3. 工作流先按“我要做什么”(任务胶囊)缩小,再按模型族分组,置顶永远在最上;长文件名只在悬停/表头次要位置出现,主名是人话名。
|
||||
4. “待配置”的工作流默认隐藏(开关「仅可运行」),避免把 146 个未映射文件冒充可用工具;关掉开关能看到并引导去映射。
|
||||
5. 智能体不是聊天:任务列表 + 计划 + 工具卡 + 成果栏三栏,成果有归属(项目·阶段)和确认流程。
|
||||
6. 管理页(任务/工作流/模型连接/素材库)共用“左筛选 220 / 中列表 / 右详情 360”,详情不跳页。
|
||||
|
||||
## 3. 设计 Token
|
||||
|
||||
```css
|
||||
/* 表面 */ --bg-0:#07080c; --bg-1:#0c0e14; --bg-2:#12151d; --bg-3:#1a1e29; --bg-4:#232838;
|
||||
/* 描边 */ --line:rgba(255,255,255,.06); --line-2:.11; --line-3:.18;
|
||||
/* 文字 */ --tx-1:#f3efe6; --tx-2:#aaa79e; --tx-3:#6f6d68; --tx-inv:#1a1206;
|
||||
/* 主光 */ --gold:#e9b04b; --gold-hi:#f8d68c; --gold-lo:#b8771f;
|
||||
--grad-gold:linear-gradient(135deg,#fbe1a1,#efb54f 45%,#d98a26);
|
||||
/* 场景色温 */ --sc-image:#3fd8c6; --sc-video:#ff8a3d; --sc-agent:#a879ff; --sc-project:#e9b04b; --sc-manage:#7f93b5;
|
||||
/* 状态 */ --ok:#43d39a; --warn:#f2b33d; --err:#ff5d6c; --info:#5aa9ff;
|
||||
/* 字号 */ --fs-display:28px/700; --fs-h1:20px/600; --fs-h2:16px/600; --fs-body:13px/400 lh1.55; --fs-small:12px; --fs-micro:11px;
|
||||
/* 字体 */ --font:"PingFang SC","HarmonyOS Sans SC","Microsoft YaHei UI",sans-serif; --mono:"JetBrains Mono",Consolas,monospace;
|
||||
/* 间距 */ --s1:4 --s2:8 --s3:12 --s4:16 --s5:20 --s6:24 --s8:32;
|
||||
/* 圆角 */ --r-xs:4 --r-sm:6 --r-md:10 --r-lg:14 --r-xl:20 --r-pill:999;
|
||||
/* 阴影 */ --sh-1 内高光+2/8; --sh-2 内高光+12/32 黑50%; --sh-gold 金色外发光(仅主按钮);
|
||||
/* 动效 */ --ease-out:cubic-bezier(.22,1,.36,1); --ease-io:(.65,0,.35,1); --ease-spring:(.34,1.56,.64,1);
|
||||
--t-fast:120ms; --t-base:200ms; --t-slow:320ms; --t-reveal:700ms;
|
||||
/* 布局 */ --rail:68; --list:272; --hist:92; --head:48;
|
||||
```
|
||||
|
||||
**色彩规则**
|
||||
- 一个页面只有一个金色主按钮(生成 / 确认 / 带回项目 / 开始任务)。
|
||||
- 场景色只用于“位置与焦点”:选中边框、取景框角标、进度条、焦点光环、导航指示条;禁止做大面积底色。智能体页是唯一允许场景色做氛围(极光、光球)的页面。
|
||||
- 数字信息(编号、时间码、比例、种子、延迟)一律等宽字体。
|
||||
|
||||
## 4. 组件与状态
|
||||
|
||||
| 组件 | 尺寸 | 默认 | 悬停 | 选中/按下 | 禁用/异常 |
|
||||
|---|---|---|---|---|---|
|
||||
| 按钮 `.btn` | 26/32/38 高 | bg-3 + line-2 | bg-4 + line-3 | 下移 1px | — |
|
||||
| 主按钮 `.btn.primary` | 32/38 | 金渐变 + sh-gold | 提亮 6% + 扫光一次 | 下移 1px | 去饱和 60% 亮度,旁边红字写“还缺:…” |
|
||||
| 胶囊 `.chip` | 24–26 | bg-2 | 文字变亮 | 场景色 13% 底 + 60% 描边 | — |
|
||||
| 参数胶囊 `.pchip` | 28 | 透明 4% | 描边变亮 | 点开弹层 | — |
|
||||
| 工作流行 `.wfi` | 50 | 透明 | 光栅悬停 + 3% 底 | 场景色渐变底 + 35% 描边 | 待配置:族色块灰化、名字降一级、黄标签 |
|
||||
| 素材槽 `.slot` | 56(矮屏 50) | 虚线 line-3 | 场景色虚线 | 实线 + 缩略图 + 悬停出现 × | 必填未填:黄色虚线 |
|
||||
| 作品框 `.frame` | 适配 | 取景框角标常显 | — | — | 失败:斜纹底 + 原因 + 重试 |
|
||||
| 视频播放器 | 画面 + 40 控制栏 | **暂停**,中央大播放键 | 大播放键放大 1.08 | 播放后大键隐藏,控制栏常显 | — |
|
||||
| 草稿条 | 44 | 紫色渐变 + 描边 | — | — | — |
|
||||
| 弹层 `.pop` | ≤620×64vh | 底部固定“取消/保存”,内部滚动 | — | — | — |
|
||||
|
||||
**交互铁律**
|
||||
- 视频永不自动播放;切换结果保持暂停;空格只在播放器获得焦点时切换。
|
||||
- 生成按钮、确认按钮、带回项目按钮永远在首屏固定位置,不随内容滚走。
|
||||
- 参数弹层“取消不改参数”;图片与视频分别记忆参数。
|
||||
- 切换工作流保留提示词;新工作流有反向提示词时自动出现右栏(左正右反)。
|
||||
- 置顶不切换当前工作流、不清空输入;置顶分图片/视频保存。
|
||||
|
||||
## 5. 图标与插图
|
||||
|
||||
- 图标:1.7px 线性描边、圆角端点、24 网格;尺寸 14/18/22。不用填充图标,置顶激活例外(填充金色)。
|
||||
- 工作流族色块:34px 圆角方块 + 族缩写(Qwen/Kr2/FLX/Z/H3/Wan/GPT/NB/可灵/即梦),待配置时灰化。
|
||||
- 插图:阶段助手卡与工作室大卡用真实项目产物做底图(无产物时用场景色渐变 + 胶片颗粒),不用卡通吉祥物。
|
||||
|
||||
## 6. 动效规范
|
||||
|
||||
| 名称 | 触发 | 时长 · 曲线 | 可打断 | 减少动效 |
|
||||
|---|---|---|---|---|
|
||||
| 页面进入 | 切一级页 | 320ms · ease-out,上移 6px 淡入 | 是 | 直接出现 |
|
||||
| 光栅悬停 | 指针进入列表行/卡片 | 320ms 渐显,跟随指针的光斑 + 蜂窝纹 | 是 | 只变背景亮度 |
|
||||
| 取景框 | 选中工作流/作品/历史 | 200ms · ease-out,角标外扩 2px | 是 | 保留角标 |
|
||||
| 扫描光束 | 生成中 | 1.6s 循环 · ease-io | 取消即停 | 只留进度条 |
|
||||
| 显影 | 结果到达 | 700ms · ease-out(模糊/过曝 → 清晰) | 点击跳过 | 淡入 |
|
||||
| 主按钮扫光 | 悬停主按钮 | 700ms 一次 | 是 | 关闭 |
|
||||
| 弹层 | 打开参数/菜单 | 200ms · 上移 6px + 缩放 .98 | Esc / 点外部 | 直接出现 |
|
||||
| 提示 Toast | 操作完成 | 320ms spring,3.2s 消失 | 是 | 淡入 |
|
||||
| 智能体氛围 | 进入智能体页 | 极光 18s 漂移、粒子上升、光球呼吸 4.5s | 离开页面即停 | 全部关闭 |
|
||||
|
||||
**性能降级**:粒子用单 canvas、≤46 个,离开页面停止 rAF;极光用 3 个模糊圆、transform 动画;`prefers-reduced-motion` 或侧栏「动效」开关关闭时加 `html.reduce`,去掉所有位移/循环,仅保留颜色与状态变化。
|
||||
|
||||
## 7. 桌面断点
|
||||
|
||||
| 窗口宽 | 总导航 | 工作流列 | 历史栏 | 其他 |
|
||||
|---|---|---|---|---|
|
||||
| ≥1600 | 68 | 272 | 92 | 工作室右栏 340,智能体成果栏 420 |
|
||||
| 1367–1599 | 68 | 272 | 92 | 标准(1440×900 样稿) |
|
||||
| 1281–1366 | 68 | 248 | 84 | 页头隐藏工作流文件名 |
|
||||
| ≤1280 | 60 | 236 | 76 | 页头 44;历史栏可收起 |
|
||||
| 高 ≤800 | 导航压缩 | 提示词 48 高、素材槽 50;长提示词点「展开」到 28vh,内部滚动 |
|
||||
|
||||
最小支持 1280×720(已出样稿 `14-create-image-1280x720.png`、`15-agent-task-1280x720.png`)。不做手机版。
|
||||
|
||||
## 8. 待确认事项(请总管转白栖知)
|
||||
|
||||
1. **主题方向**:「夜片场」(墨黑 + 钨丝金 + 各区场景色温)是否认可?这是本卡唯一主方向,不再并列给多套。
|
||||
2. **生图/生视频合并**为一个「创作台」,顶部切换图片/视频(各自记忆)——是否同意?
|
||||
3. **试验台(Playground)并入创作台**、旧入口下线;**三维搭景**不进一级导航,入口放在项目「镜头脚本」阶段和视频参考槽——是否同意?
|
||||
4. 「仅可运行」默认打开:146 个未映射文件默认不出现在创作台,只在「工作流库」里显示待配置——是否接受?
|
||||
5. ComfyUI 连接与模型设置合并为「模型连接」一页四个页签——是否同意?
|
||||
6. 云端图片/视频模型(GPT-Image-2、Nano Banana、可灵、即梦)在样本中按“已接入”展示;当前后端只开放 LLM,接入属于新增能力,需另行排期。
|
||||
|
||||
## 9. 施工提示(给总工/施工员)
|
||||
|
||||
- Token 直接替换现有 `app.css` 变量体系;组件按第 4 节状态表逐一对照。
|
||||
- 舞台尺寸用容器实测(ResizeObserver)计算 contain 尺寸,不用固定高度;视频控件高度计入。
|
||||
- 历史栏、工作流列、成果栏、弹层内容都各自滚动;页面本身不滚(工作室、规范页除外)。
|
||||
- 验收看图看操作:1440×900、1366×768、1280×720 三档下,作品完整、主按钮可见、视频默认暂停。
|
||||
@@ -0,0 +1,105 @@
|
||||
# 夜片场 · 多主题规范 v2(待白栖知看样确认)
|
||||
|
||||
> 视觉总监出具,在 HEL-716《夜片场》规范 v1 的基础上**增补**主题。v1 中的布局、组件、动效和断点全部继续有效,本文件只写新增和改动的部分。样本使用的是示例画面和演示状态,不调用真实生成、不产生费用。
|
||||
|
||||
## 0. 原则
|
||||
|
||||
**同一个片场,换一种灯光。** 四套主题共用同一套布局、功能、操作顺序和动效,只改变语义变量和少量“气质”参数(圆角、发光开关、氛围层)。组件 CSS 里只能引用语义变量,不允许写死颜色;主题差异只能写在 `[data-theme="…"]` 块中。
|
||||
|
||||
| ID | 中文名 | 气质与适用场景 | 默认 |
|
||||
|---|---|---|---|
|
||||
| `night` | **夜片场** Nocturne | 墨黑底、钨丝金主键、各工作区有自己的场景色温。夜间、沉浸式创作。 | ✅ 默认(即已认可的原版,一个像素都没改) |
|
||||
| `day` | **日光棚** Daylight | 暖白纸面、墨色文字、深琥珀主键;不使用发光,靠描边、实色和对比度区分。白天办公、长时间审稿。 | |
|
||||
| `grade` | **调色间** Grading | 中性深灰、白色主键、场景色降低饱和度、舞台不加色调;圆角收紧到 2–8px,更像工具。用于判断作品色彩,适合长时间看片。 | |
|
||||
| `bluehour` | **蓝调时刻** Blue Hour | 靛蓝到紫的天色渐变、珊瑚橙主键、圆角放大到 8–26px、主按钮做成胶囊形,氛围最浓。用于提案、展示和灵感发散。 | |
|
||||
|
||||
## 1. 语义变量(施工直接使用)
|
||||
|
||||
基础变量名沿用 v1;v2 新增的语义变量标 ★。
|
||||
|
||||
| 语义 | 变量 | 夜片场 | 日光棚 | 调色间 | 蓝调时刻 |
|
||||
|---|---|---|---|---|---|
|
||||
| 页面底 / 输入凹槽 | `--bg-0` | #07080c | #e9e4da | #1b1b1c | #0a1029 |
|
||||
| 面板(导航、列表、历史栏) | `--bg-1` | #0c0e14 | #f4f1ea | #222223 | #0f1838 |
|
||||
| 卡片 | `--bg-2` | #12151d | #fbf9f5 | #29292b | #162148 |
|
||||
| 浮起层 / 按钮 | `--bg-3` | #1a1e29 | #ffffff | #323234 | #1f2c5c |
|
||||
| 悬停 | `--bg-4` | #232838 | #efe9df | #3c3c3f | #2a386e |
|
||||
| 主文字 | `--tx-1` | #f3efe6 | #1f1b15 | #ecebe8 | #eef1fb |
|
||||
| 次要文字 | `--tx-2` | #aaa79e | #55504a | #acaba6 | #a9b1cc |
|
||||
| 弱文字(≥4.3:1,浅色主题同样适用) | `--tx-3` | #6f6d68 | #736d65 | #85847f | #7680a2 |
|
||||
| 叠加通道 ★(悬停、细线用 `rgb(var(--ink)/a)`) | `--ink` | 255 255 255 | 38 30 18 | 255 255 255 | 214 224 255 |
|
||||
| 边框 1/2/3 | `--line*` | 白 .06/.11/.18 | 墨 .09/.15/.26 | 白 .075/.13/.22 | 冷白 .07/.12/.2 |
|
||||
| 主操作(“去做”,全页唯一) | `--gold` / `--grad-gold` | #e9b04b 金 | #a65a0c 深琥珀 | #d9dde2 校色白 | #ff9a6c 珊瑚 |
|
||||
| 主操作上的文字 | `--tx-inv` | #1a1206 | #ffffff | #141516 | #2b0c05 |
|
||||
| 场景 · 图片 | `--sc-image` | #3fd8c6 | #0b8378 | #6fc2b7 | #4fd6ea |
|
||||
| 场景 · 视频 | `--sc-video` | #ff8a3d | #c2511a | #d8a06c | #ffbb55 |
|
||||
| 场景 · 智能体 | `--sc-agent` | #a879ff | #6d45d0 | #a598d8 | #b592ff |
|
||||
| 场景 · 项目 | `--sc-project` | #e9b04b | #a65a0c | #d4bc8a | #ff9a6c |
|
||||
| 场景 · 管理 | `--sc-manage` | #7f93b5 | #4c6286 | #909cae | #8aa0dc |
|
||||
| 场景 · 素材资产 ★ | `--sc-asset` | #ff86b0 | #b93a72 | #d394b0 | #ff86c0 |
|
||||
| 成功 | `--ok` | #43d39a | #11804f | #4cc38f | #4fd69c |
|
||||
| 提醒 / 待配置 | `--warn` | #f2b33d | #9a6400 | #e3a93b | #ffc94d |
|
||||
| 错误 / 失败 | `--err` | #ff5d6c | #c42b41 | #ee5d70 | #ff4d6d |
|
||||
| 信息 / 历史回看 | `--info` | #5aa9ff | #2263c2 | #5d9fe3 | #69b4ff |
|
||||
| 场景色文字混色端 ★ | `--hi` | white | #141109 | white | white |
|
||||
| 场景/状态实底上的文字 ★ | `--on-scene` / `--on-ok` | 深色 | #fff | 深色 | 深色 |
|
||||
| 输入框底 ★ | `--well` | bg-0 | #ffffff | #18181a | bg-0 |
|
||||
| 页头毛玻璃 ★ | `--glass` | bg-1 70% | bg-1 88% | bg-1 82% | 定制 55% |
|
||||
| 遮罩层 ★ | `--scrim` | 黑 .6 | 暖墨 .34 | 黑 .55 | 深蓝 .62 |
|
||||
| 作品阴影 ★ | `--media-shadow` | 重 | 轻、暖 | 中 | 重、偏蓝 |
|
||||
| 发光开关 ★ | `--glow` | 1 | 0 | 0 | 1 |
|
||||
|
||||
另有特效变量 ★:`--gen-bg`(生成中底色)、`--beam`(扫描光)、`--grain`(颗粒)、`--lattice`(光栅蜂窝纹)、`--scanline`、`--stage-tint`(舞台场景色晕 0–10%)、`--aur-1/2/3`、`--aur-op`(极光)、`--agent-hero`、`--agent-title`、`--orb-core`、`--particle`、`--brand-grad/-glow/-ink`、`--display-grad`、`--fail-a/b`。完整取值见交互样本 `<style>` 中的四个主题块;也可以在样本「规范 → 05 主题」里查看,那里直接读取实际变量。
|
||||
|
||||
**场景色与状态色**:两组颜色在每套主题里都单独取值,不互相借用。场景色只表示“你在哪个工作区”(选中边框、取景框、进度、焦点);状态色只表示“结果怎么样”。调色间里场景色降低饱和度,状态色保持鲜明,两者不会混淆。
|
||||
**不随主题变的元素**:作品框底色、视频控制条、作品上的角标(编号、时长、草图标签)、灯箱,在所有主题都保持深色,避免主题给作品染色。工作流模型族色块也保持不变,方便跨主题识别。
|
||||
|
||||
## 2. 特色效果按主题适配
|
||||
|
||||
| 效果 | 夜片场 | 日光棚 | 调色间 | 蓝调时刻 |
|
||||
|---|---|---|---|---|
|
||||
| 扫描光(生成中) | 场景色 + 白光 | 场景色 24% + 纸白高光,不发光 | 中性白,无色彩 | 场景色 34% + 暖白 |
|
||||
| 显影(结果到达) | 模糊/过曝 → 清晰 | `developDay`:模糊 + 棕褐色调 → 清晰(像相纸显影) | `developGrade`:去饱和 → 清晰 | 同夜片场 |
|
||||
| 取景框角标 | 场景色偏白 | 场景色偏墨(更深) | 场景色 | 场景色 |
|
||||
| 光栅悬停 | 白色蜂窝纹 | 墨色蜂窝纹 | 白色 | 冷白色 |
|
||||
| 发光(导航条、进度条、阶段下划线、状态点) | 有 | **无** | **无** | 有 |
|
||||
| 智能体氛围 | 紫粉青极光 .33 | 淡紫/桃/薄荷 .42 | 低饱和 .16 | 蓝/珊瑚/青 .36 |
|
||||
|
||||
浅色主题不靠发光,也不靠低对比文字来区分信息:选中用实色描边加浅底,禁用的主按钮用浅灰底加灰字(不只是降低透明度)。
|
||||
|
||||
## 3. 切换与记忆规则
|
||||
|
||||
- **入口**:侧栏底部「主题」(图标是当前主题的四色圆点)→ 弹层列出四个主题:实时缩略预览、中文名、英文名、色调和一句话用途,当前项有 ✓。「设置 → 外观主题」有同样的四张卡片。键盘可用:方向键在选项间移动,Enter 选择,Esc 关闭并把焦点还给入口按钮。
|
||||
- **只改 `html[data-theme]`**:不重新渲染页面,所以提示词、上传素材、参数、工作流选择、历史回看、项目阶段草稿、智能体任务和成果都保持原样(样本自测已覆盖,见第 6 节)。
|
||||
- **记忆**:存 `localStorage['aigc-theme']`;`<head>` 里的内联脚本在首次绘制前读取,避免先闪一下夜片场。读到非法值或读取失败时回退到 `night`。
|
||||
- **存储不可用**:切换照常生效,弹层底部提示“浏览器禁止本地存储:本次有效,其他操作不受影响”,不报错,也不阻止其他操作。
|
||||
- **默认是夜片场**,不跟随系统的深浅色设置(深浅色只是主题的一个属性,不是选择主题的依据)。正式产品如需按账号同步,再把同一个值存到用户设置里,本地存储作为缓存。
|
||||
- 「动效开/关」已移到主题弹层底部(同属外观设置),侧栏底部仍然是 4 个按钮,1280×720 下不会溢出。
|
||||
|
||||
## 4. 动效与减少动效
|
||||
|
||||
| 名称 | 触发 | 时长 · 曲线 | 可否打断 | 减少动效时 |
|
||||
|---|---|---|---|---|
|
||||
| 切换主题「开灯」 | 选择新主题 | 520ms · `--ease-out`;新主题从点击位置圆形展开(View Transitions) | 是,再次选择会接管 | 立即替换 |
|
||||
| 不支持 View Transitions 时 | 同上 | 280ms 颜色淡入淡出(`html.theme-anim`,340ms 后移除) | 是 | 立即替换 |
|
||||
| 其余动效 | 同 v1 | 同 v1 | 同 v1 | 同 v1 |
|
||||
|
||||
`prefers-reduced-motion` 或手动关闭动效时加上 `html.reduce`:主题立即切换,极光、粒子、光束隐藏,只保留颜色和状态变化。粒子颜色在切换主题时从 `--particle` 重新读取。
|
||||
|
||||
## 5. 本卡同时修正(按已定分工)
|
||||
|
||||
- 云端 GPT-Image-2、Nano Banana Pro、可灵、即梦:在创作台归入「云端模型 · 尚未接入(后续批次)」,灰色色块加虚线「未接入」标签;选中后,生成按钮禁用并提示“尚未接入:云端接口属后续批次,当前不可生成”,页头连接状态为灰点“云端接口尚未接入”。「模型连接」页的云端卡片改为虚线卡片“未接入 / 等待接入”,图片模型、视频模型页签标注“后续”。历史和任务里不再出现云端生成的结果。
|
||||
- 三维搭景和试验台的组织方式标签由“待确认”改为“已确认”。
|
||||
- 工作流库表格改为固定列宽,长文件名用省略号截断,1366 宽时“状态”列不再被挤出可视区。
|
||||
|
||||
## 6. 检查结果
|
||||
|
||||
- 尺寸:四套主题都检查了 1440×900、1366×768(视频首尾帧)和 1280×720(图片竖图)。主按钮在首屏、作品按比例完整显示、视频默认暂停、列表各自滚动、侧栏底部不溢出(见附图 04、05)。
|
||||
- 全站一致性:工作室、创作台(图片/视频)、智能体首页/任务、项目工作台、工作流库、模型连接、设置、规范页都只使用语义变量(附图 02、03、10–16)。
|
||||
- 自测(样本地址加 `?selftest=1`,在无界面浏览器中运行):17/17 通过,包括在四套主题之间依次切换后提示词、上传、参数、工作流、历史回看、项目阶段和智能体状态不变;切换后选择被记住;存储被禁止时仍能切换且有提示;云端未接入时生成按钮被禁用。另外验证了重新打开页面会沿用上次的选择。
|
||||
|
||||
## 7. 施工提示
|
||||
|
||||
1. 先把 v1 组件中写死的 `rgba(...)` 和十六进制颜色换成本文的语义变量(样本已完成,可直接对照)。规则是:白色叠加 → `rgb(var(--ink)/a)`;场景色/状态色叠加 → `color-mix(in srgb,var(--x) N%,transparent)`;场景色文字 → 与 `var(--hi)` 混合。
|
||||
2. 主题块的选择器写成 `[data-theme="x"]`,不要写成 `html[...]`,这样局部预览(缩略图、规范对照表)也能套用主题。
|
||||
3. 新主题须等白栖知看样确认后再正式施工;最终上线哪几套、是否保留切换入口,以他的确认为准。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
# HEL-729 · B1 夜片场创作台
|
||||
|
||||
从 B0 已审核、已部署的 `8e84e4c21f22febe7bd4f20777d06fa1d4914e8e` 继续,目标分支为 `feat/hel-728-night-foundation`。本批只改创作台与其兼容入口,不改生成 API、数据库、连接配置与项目确认语义,不部署。
|
||||
|
||||
## 实现
|
||||
|
||||
- `/create` 成为真实创作台,内部切换图片/视频。旧 `/image`、`/video`、`/playground` 保留所有查询参数与片段转入创作台。
|
||||
- 复用 StudioDesk、Composer、SlotTray、ParamsPanel、ParameterPopover、TakeFeed 和真实生成/回流 API。工作流列采用 B0 的 `--list`,搜索、用途/模型筛选、收藏与置顶保留;仅可运行默认开启,关闭后待配置项进入对应的真实映射/测试管理行。
|
||||
- 草稿按媒介和来源分别保存,内部工作流独立记忆输入及连接。首次切换工作流延续正反向提示词;显式深链优先于不同请求的缓存,同一请求刷新恢复用户的选择,异步详情与列表加载顺序不改变归属。存储受限时保留同标签页内存回退。
|
||||
- 素材与提示词并排放在底座,长提示词可展开并内部滚动。参数弹层沿用草稿取消/保存。槽尺寸与提示词高度统一归入 `static/themes.css`,四套主题继续可用。
|
||||
- 历史是独立右栏,可收起。回看只改变观看对象,显示历史状态条;返回当前创作保留原输入。主动复用才回填,跨媒介复用切到对应编辑器,并恢复生成时使用的可用连接。
|
||||
- 作品按容器实测尺寸与媒体宽高计算 contain 大小。视频无自动播放,中央大播放键清晰,空格仅在视频自身获得焦点时控制;切换结果暂停。下载、取消、重试、详情及已有来源项目回流均沿用原链路。
|
||||
|
||||
## 自测与证据
|
||||
|
||||
`e2e/run-hel729.py` 在前台启动并收集隔离 FastAPI、假 ComfyUI、假 LLM 与真实前端,结束清理子进程。测试断言数据目录为本次隔离目录,未读取用户数据或调用收费服务。测试输入、遮罩、首尾帧、多参考素材通过真实上传、队列与输出保存链路;来源结果通过原 attach-workbench 接口显式回流。
|
||||
|
||||
浏览器脚本 `e2e/hel729.cjs` 检查三档桌面(1440×900、1366×768、1280×720)、横竖图完整显示、首屏主操作、手动播放、图视频与工作流输入归属、刷新、主题切换、参数取消/保存、历史与复用、失败重试、取消和存储受限。PNG 和 results.json 通过任务卡附件交付;样图均为隔离测试内容。
|
||||
|
||||
相关后端回归:`test_image_input_workflows.py`、`test_playground_uploads.py`、`test_playground_workbench_flowback.py`、`test_playground.py` 共 47 项通过。前端 `npm run check`、`npm run test`、`npm run build` 通过;类型检查保留基线 ShotStagePanel 视频字幕警告一条。
|
||||
|
||||
全仓 `npm run lint` 仍被既有格式问题阻断,直接 ESLint 也因仓库缺少 eslint.config 配置退出;这两项为 B0 既有工具链问题,不在本批扩面格式化或新增依赖。本批主要组件另做 Prettier 定向检查和 `git diff --check`。
|
||||
|
||||
## 尚未覆盖
|
||||
|
||||
未连接真实 ComfyUI/GPU 或云模型做付费生成,未部署测试站。通用“选择项目/阶段存入”和智能体任务成果能力仍按分批安排留给 B4。本批交总工独立对照样本审核,不能以自动化数量替代视觉验收。
|
||||
@@ -0,0 +1,27 @@
|
||||
# HEL-740 · B1 创作台样本返工
|
||||
|
||||
继续原 `feat/hel-729-creative-desk`,以前次审核 head `2304ab1538a4fd5f301efbf58f1a28b987b78809` 为起点;PR #3 目标仍为 `feat/hel-728-night-foundation@8e84e4c21f22febe7bd4f20777d06fa1d4914e8e`。未部署、合并或修改生成 API、数据库和正式数据。
|
||||
|
||||
## 实现
|
||||
|
||||
- `static/themes.css` 统一底座、按钮、输入与弹层 token。夜片场/日光棚按钮 131×38、r10、14px/700;调色间遵样本 r4,蓝调时刻遵样本胶囊。底座渐变、边框、阴影、距两边16/底14恢复,样本浮层圆角随主题变化。
|
||||
- `Composer` 恢复60px/13px正负提示词、1.35:1比例与计数栏,去掉固定滚动槽占位。1440有媒体输入时正375.3×60、负269.7×60,对原样本375.6/270误差小于1px。长文本内部滚动/展开与真实输入要求保留。
|
||||
- `Modal` 增加可选底座锚点与头部操作槽,复用原焦点层栈、Tab循环、Escape、外部点击和焦点归还。参数620×560;较小窗口按64vh与顶栏可用空间缩短、内容滚动。恢复默认只改草稿,取消丢弃、保存才应用。
|
||||
- `Artboard` 恢复媒体下40px行内八入口,放大/下载/复用参数/作为参考图/送去生视频/存入项目/素材库/更多。真实来源项目回流沿用原接口;更多保留详情、对比、尾帧导出等。视频默认暂停、contain完整显示、中央64px播放按钮与仅播放器聚焦空格保留。
|
||||
- `DeskWorkflowRail`/`HistoryRail` 恢复标题行历史切换、收起归零、247×36分段控件。`StudioDesk` 恢复胶囊历史提示、14px/600工作流名和头部连接状态;连接名/延迟来自真实配置与测试结果,未知显示未知。
|
||||
|
||||
## 验证
|
||||
|
||||
- `e2e/hel729.cjs` 42项全过,只按恢复后的真实入口更新记录/来源项目选择器;覆盖上传、遮罩、首尾帧、多参考、草稿隔离、历史不改输入、主动/跨媒介复用、旧入口、来源项目回流与手动播放。
|
||||
- `e2e/hel740.cjs` 69项全过:三档尺寸×四主题逐项尺寸/比例、首屏/contain、八入口、锚定弹层及恢复默认/取消/保存/焦点、历史归零/胶囊、视频暂停播放。附原HTML与实际应用并排图、完整DOM实测JSON和全程WebM,另含悬停、选中、生成中与播放逐帧。
|
||||
- 隔离FastAPI+假ComfyUI;后端四组相关测试47项全过。`check` 0错误、1条既有ShotStagePanel字幕警告;`test`、`build`、定向格式检查、`git diff --check`通过。全仓lint仍受63个文件基线格式与缺失ESLint配置影响,不扩全仓治理。
|
||||
|
||||
复现:下载主卡已确认HTML,通过 `HEL740_SAMPLE` 指定原样本路径;在原隔离栈运行 `HEL729_TESTS=e2e/hel729.cjs,e2e/hel740.cjs python3 e2e/run-hel729.py`,环境按原脚本配置Playwright/Chromium。截图和日志通过任务卡附件交付,不将临时数据提交进仓库。
|
||||
|
||||
## 明确边界与剩余事实
|
||||
|
||||
- 默认文生图没有媒体输入,样本可选垫图槽位仍待总管组织决定;未新增映射或伪造上传槽。此次同状态尺寸对照使用已支持图片和遮罩的真实工作流类型。
|
||||
- 参数字段来自工作流声明;隔离夹具仅有种子和一个高级参数,没有伪造样本中的分辨率、步数等能力。实际工作流声明这些字段时沿用动态表单显示。
|
||||
- 跨工作流送图能力未具备,送去生视频入口禁用并解释;通用项目/阶段选择留B4,存入项目只在真实来源上下文存在时启用。
|
||||
- 已接受B0的48px顶栏保留,其造成的作品高度差不计作已还原。没有宣称全页逐像素一致或交部署。
|
||||
- PR #3为AGit请求,已用原topic `hel-729-creative-desk` 推送到 `refs/for/feat/hel-728-night-foundation` 更新现有审核引用(仅推送普通分支不会更新该请求);Gitea API `/api/v1/user` 返回401,本运行只有SSH推送身份,不能修改PR标题/正文。任务卡附拟更新文案供总管完成元数据更新,不新建重复请求。
|
||||
@@ -0,0 +1,13 @@
|
||||
# HEL-741 创作台 R1—R4 剩余呈现修复
|
||||
|
||||
沿用 feat/hel-729-creative-desk 和原 PR #3,基于 7bf63d05873b1e473905763d13fe999ac688cc6d。目标分支仍为 feat/hel-728-night-foundation@8e84e4c21f22febe7bd4f20777d06fa1d4914e8e。未部署、未合并、未新增依赖、未修改后端或正式数据。
|
||||
|
||||
- DeskWorkflowRail 复用统一 token,原用途/家族选择改为 chips;保留收藏、仅可运行、搜索、置顶及待配置导向;仅可运行按原样本26×15开关绘制,保留原生复选框语义与键盘操作。搜索加入原图标结构,34px/13px;metadata 展示实时筛选数,仓库文件数因接口未知如实说明,不用数据库条数代替。真实用途仍为现有五类;没有独立局部重绘元数据,不臆测新分类。
|
||||
- StudioDesk 头行按样本48px、1280下44px;名字裸14px/600、21.6875px行盒。家族标识、用途、等宽源文件名、真实连接信息保留。源文件名来自已有source_path,无值显示未知。管理详情入口复用侧栏页脚,参数仍由dock chips进入,专注移入参数层;弹层焦点归还更新为真实入口。
|
||||
- Artboard 子按钮依样本夜/日/调色r5、蓝调r8;两条1px×18px分隔线、尾部26px图标删除。已有作品编号处打开详情、对比、尾帧导出等能力。删除先确认,再调用原5秒撤销队列;提示如实说明生成文件和项目回流记录也会删除,后端规则不变。
|
||||
- Composer 生成图标14px。蓝调按钮token为140.7px,浏览器140.6875px;样本140.65625px。夜/日/调色仍保留已验收131px,对样本130.65625px有0.34375px差值,不填入虚构的演示计费后缀。
|
||||
- 原样本的主题差异通过token保持:搜索框圆角6/6/4/8,筛选chips圆角999/999/3/999,连接胶囊调色间r3。样本原件保持不动。
|
||||
|
||||
验证:check 0错误、1条既有ShotStagePanel字幕警告;npm test/build通过;后端生成、图像输入、项目回流、连接四组67项通过。浏览器在隔离FastAPI和假ComfyUI运行,原42项回归和新增138项检查均通过,覆盖三档×四主题测量、筛选/入口/专注/焦点/删除确认取消撤销及真实提交;逐项结果、实测值、并排截图和录屏由任务附件交付。全仓lint仍有62个既有格式文件且ESLint缺少配置,定向格式通过,不做全仓治理。
|
||||
|
||||
真实数据与样本的剩余差异明确列于附件,不宣称整页差异清零;全站B0顶栏保留。通用存入项目留B4,跨工作流送视频如实禁用;文生图不具备媒体输入时没有垫图槽,符合主卡最新“文生图不垫图”决定。
|
||||
@@ -0,0 +1,24 @@
|
||||
# HEL-743 搜索提示与头行定点返工
|
||||
|
||||
基于 `e62499c517ccfbd78e7d3c13df98199b6699a6b2`,沿用分支 `feat/hel-729-creative-desk` 和 PR #3,base 保持 `feat/hel-728-night-foundation@8e84e4c21f22febe7bd4f20777d06fa1d4914e8e`。
|
||||
|
||||
- `DeskWorkflowRail.svelte` 补原件 `/` kbd,字体、内边距、圆角进入 `static/themes.css`,颜色和边框复用现有主题 token。输入按原件flex:1分配剩余宽度。最终输入样本/实际为176.671875/176.6875px(另两档152.671875/152.6875、140.671875/140.6875),有1/64px排版量化差,未把实测值写成完全相等;kbd字体/内边距/圆角/边框/颜色均一致。非编辑态 `/` 聚焦搜索,编辑器、contenteditable、组合输入、修饰键和可见弹层不会劫持;Esc 保留筛选并归还先前焦点,无可用来源时回到工作流栏控制按钮。
|
||||
- `StudioDesk.svelte` 修改原头行规则,左右负边距和内边距各16px,背景、底线通到内容列边缘;使用统一玻璃和线条 token,蓝调使用原件55%玻璃背景。内容起点、头行高度及真实连接信息保持。
|
||||
- 源文件名增加原件 `max-width:1366px` 隐藏规则,1440显示真实source_path的文件名,完整路径仍在title。未伪造业务数据。
|
||||
- 从 HEL-742 复审的本地材料读取并纳入 `e2e/hel742.cjs`,此前任务附件确实只有5张PNG,脚本并非从附件下载。保留无图片声明时无槽位/素材入口、图视频切换、遮罩双槽对照7项。
|
||||
- `e2e/hel743.cjs` 新增三档×四主题几何/玻璃/底线/kbd/文件名测量,1367/1366断点、搜索/焦点/输入法/弹层回归。只在隔离数据库中把夹具实际JSON文件的路径写为source_path,以验证真实文件名展示。`e2e/compare-hel743.py` 将测量区域并排,左样本、右实际。
|
||||
|
||||
复跑(沿用已有依赖,不新增依赖):
|
||||
|
||||
```bash
|
||||
# HEL740_SAMPLE 指向从主卡独立下载且SHA256核对的原件
|
||||
# Linux按本机环境提供 PLAYWRIGHT_MODULE_PATH / CHROMIUM_EXECUTABLE_PATH / LD_LIBRARY_PATH
|
||||
HEL740_SAMPLE=/path/to/approved.html \
|
||||
HEL729_TESTS=e2e/hel729.cjs,e2e/hel740.cjs,e2e/hel742.cjs,e2e/hel743.cjs \
|
||||
calliope-backend/.venv/bin/python e2e/run-hel729.py
|
||||
calliope-backend/.venv/bin/python e2e/compare-hel743.py
|
||||
```
|
||||
|
||||
原件 SHA256:`0473d80d22b90194375f3566c1b8ca30dbd7433581b18f60956eabca252242a4`。runner在前台运行,finally停止并回收本轮服务;隔离夹具为 `e2e/shots/reference`。并排图、逐项结果和实测值随本卡附件交付。
|
||||
|
||||
验证:前端 check 0错误、1条既有ShotStagePanel字幕警告;test/build通过;相关后端4组47项通过。全仓lint仍为既有62文件格式问题、ESLint缺少配置;本次组件定向Prettier和 `git diff --check` 通过。浏览器42项原回归、138项R1—R4回归、7项模式切换和91项本轮定向断言通过(结果和实测值见任务附件)。中文输入法组合态用合成isComposing及229事件验证,未接入真实中文输入法/GPU/云生成。浏览器首次尝试因运行库路径不完整退出,修正本地LD_LIBRARY_PATH后正常执行;新增脚本曾因旧“参数”按钮名超时,已改用现有“创作设置⌄”入口复跑。没有修改正式数据、后端、B0顶栏或已裁定非偏差项;没有收费生成、合并或部署。
|
||||
@@ -0,0 +1,5 @@
|
||||
c32f1f7e6814350d2f5470e900b5a697bf8f34f02eac3837eca9a7a3fe9b25d4 /opt/aigc-home/releases/formal-v5-35fd07a.tgz
|
||||
f1fb2e4167a7e552de69f7857347ca95a1ebf0f1a5ab1d075692ccf1ae5a103b /opt/aigc-home/releases/backup-HEL665-v5-35fd07a-20261015.tgz
|
||||
8374951e292df80f43aac3425a7aaa312e59f3a0b953b45a4d051934612ff304 /opt/aigc-home/releases/rollback-HEL665-v5-35fd07a-20261015.sh
|
||||
old-source-present
|
||||
old-static-present
|
||||
@@ -0,0 +1,31 @@
|
||||
{"image_wf": 1, "video_wf": 2, "endpoint": "cba0eaa4-57f2-4f57-99b1-ba7b6f0b0835"}
|
||||
{'dual': 3, 'h3': 4, 'project': 1}
|
||||
RUN e2e/reference/check.cjs
|
||||
PASS 真实工作流显示正反向左右分栏
|
||||
PASS 工作流列表可应对超过 48 项
|
||||
PASS 列表搜索过滤真实元数据
|
||||
PASS 置顶不切换工作流、不清空草稿
|
||||
PASS 参数取消不改当前值
|
||||
PASS 参数保存应用到当前工作流
|
||||
PASS 真实生成请求提交且取得结果
|
||||
PASS 正反向与种子写入任务
|
||||
locator.click: Timeout 12000ms exceeded.
|
||||
Call log:
|
||||
[2m - waiting for locator('.result-item').first()[22m
|
||||
|
||||
at /home/xiaomo/multica_workspaces/hello-af23464d222e/hel-728-a07b128af8f7/workdir/aigc-home/e2e/reference/check.cjs:146:45 {
|
||||
log: [ " - waiting for locator('.result-item').first()" ],
|
||||
name: 'TimeoutError'
|
||||
}
|
||||
RUN e2e/reference/check-copy-fallback.cjs
|
||||
locator.waitFor: Timeout 15000ms exceeded.
|
||||
Call log:
|
||||
[2m - waiting for getByRole('button', { name: /编辑内容并带回项目/ }) to be visible[22m
|
||||
|
||||
at /home/xiaomo/multica_workspaces/hello-af23464d222e/hel-728-a07b128af8f7/workdir/aigc-home/e2e/reference/check-copy-fallback.cjs:44:54 {
|
||||
log: [
|
||||
" - waiting for getByRole('button', { name: /编辑内容并带回项目/ }) to be visible"
|
||||
],
|
||||
name: 'TimeoutError'
|
||||
}
|
||||
FAILED SCRIPTS ['e2e/reference/check.cjs', 'e2e/reference/check-copy-fallback.cjs']
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
> calliope-web@1.5.8 lint
|
||||
> prettier --check . && eslint .
|
||||
|
||||
Checking formatting...
|
||||
[warn] package-lock.json
|
||||
[warn] package.json
|
||||
[warn] scripts/check-storage-ssr.mjs
|
||||
[warn] src/app.html
|
||||
[warn] src/hooks.client.ts
|
||||
[warn] src/hooks.server.ts
|
||||
[warn] src/lib/actions/spotlight.ts
|
||||
[warn] src/lib/agentComposer.ts
|
||||
[warn] src/lib/agentTasks.ts
|
||||
[warn] src/lib/assetPicker.ts
|
||||
[warn] src/lib/comfy/classifyInput.ts
|
||||
[warn] src/lib/comfy/parser.ts
|
||||
[warn] src/lib/comfy/promptInput.ts
|
||||
[warn] src/lib/comfy/resolution.ts
|
||||
[warn] src/lib/comfy/types.ts
|
||||
[warn] src/lib/comfy/useUpload.svelte.ts
|
||||
[warn] src/lib/components/agent/agentPalette.ts
|
||||
[warn] src/lib/components/generate/context.ts
|
||||
[warn] src/lib/components/generate/schema.ts
|
||||
[warn] src/lib/components/generate/takeDelete.svelte.ts
|
||||
[warn] src/lib/components/generate/takes.ts
|
||||
[warn] src/lib/components/shell/nav.ts
|
||||
[warn] src/lib/components/ui/icons.ts
|
||||
[warn] src/lib/components/ui/modalLayers.ts
|
||||
[warn] src/lib/components/ui/README.md
|
||||
[warn] src/lib/dbtime.ts
|
||||
[warn] src/lib/durationBudget.ts
|
||||
[warn] src/lib/endpointKind.ts
|
||||
[warn] src/lib/events.ts
|
||||
[warn] src/lib/formOptions.ts
|
||||
[warn] src/lib/i18n.svelte.ts
|
||||
[warn] src/lib/i18n/de.ts
|
||||
[warn] src/lib/i18n/en.ts
|
||||
[warn] src/lib/i18n/es.ts
|
||||
[warn] src/lib/i18n/fr.ts
|
||||
[warn] src/lib/i18n/ja.ts
|
||||
[warn] src/lib/i18n/ko.ts
|
||||
[warn] src/lib/i18n/zh.ts
|
||||
[warn] src/lib/jobProgress.ts
|
||||
[warn] src/lib/jobStatus.ts
|
||||
[warn] src/lib/missingOutputs.svelte.ts
|
||||
[warn] src/lib/promptTemplates.ts
|
||||
[warn] src/lib/reduceMotion.svelte.ts
|
||||
[warn] src/lib/referenceAgent.ts
|
||||
[warn] src/lib/shot/api.ts
|
||||
[warn] src/lib/shot/calibration/compositionPresets.ts
|
||||
[warn] src/lib/shot/calibration/shotSolver.ts
|
||||
[warn] src/lib/shot/helpers/cameraUtils.ts
|
||||
[warn] src/lib/shot/helpers/jointConfig.ts
|
||||
[warn] src/lib/shot/helpers/mannequinFactory.ts
|
||||
[warn] src/lib/shot/helpers/mirror.ts
|
||||
[warn] src/lib/shot/helpers/posture.ts
|
||||
[warn] src/lib/shot/helpers/primitiveFactory.ts
|
||||
[warn] src/lib/shot/motion/sampleTrack.ts
|
||||
[warn] src/lib/shot/shotStore.svelte.ts
|
||||
[warn] src/lib/shot/types/mannequin-js.d.ts
|
||||
[warn] src/lib/storage.ts
|
||||
[warn] src/lib/toast.ts
|
||||
[warn] src/lib/wideLayout.ts
|
||||
[warn] src/routes/+layout.ts
|
||||
[warn] src/routes/+page.ts
|
||||
[warn] src/routes/app.css
|
||||
[warn] src/routes/project/[id]/+page.ts
|
||||
[warn] src/routes/projects/+page.ts
|
||||
[warn] svelte.config.js
|
||||
[warn] tailwind.config.js
|
||||
[warn] tsconfig.json
|
||||
[warn] vite.config.ts
|
||||
[warn] Code style issues found in 68 files. Run Prettier with --write to fix.
|
||||
@@ -0,0 +1,413 @@
|
||||
FFFFF [100%]
|
||||
=================================== FAILURES ===================================
|
||||
________________________ test_comfy_server_info_scoping ________________________
|
||||
|
||||
def test_comfy_server_info_scoping():
|
||||
"""Native health check stays visible in both blind and linked sessions."""
|
||||
from calliope.agent.harness import tools as harness_tools
|
||||
|
||||
blind = ToolContext(session_id=1, project_id=None)
|
||||
linked = ToolContext(session_id=1, project_id=99)
|
||||
|
||||
> blind_names = {e["function"]["name"] for e in harness_tools.openai_tools_payload(blind)}
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
tests/test_agent_comfy_mcp.py:370:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
src/calliope/agent/harness/tools.py:27: in openai_tools_payload
|
||||
return _registry.openai_payload(ctx)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
src/calliope/agent/harness/registry.py:219: in openai_payload
|
||||
if self._visible(t, ctx)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
src/calliope/agent/harness/registry.py:203: in _visible
|
||||
if not user_allows_render(ctx):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
src/calliope/agent/harness/policy.py:156: in user_allows_render
|
||||
if has_structured_approval(ctx, "render"):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
src/calliope/agent/harness/policy.py:277: in has_structured_approval
|
||||
answer = latest_answer(ctx.session_id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
src/calliope/agent/harness/policy.py:260: in latest_answer
|
||||
events = read_events(session_id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
session_id = 1
|
||||
|
||||
def read_events(session_id: int) -> list[SessionEvent]:
|
||||
conn = _db()
|
||||
try:
|
||||
> rows = conn.execute(
|
||||
"SELECT * FROM agent_events WHERE session_id = ? ORDER BY seq", (session_id,)
|
||||
).fetchall()
|
||||
E sqlite3.OperationalError: no such table: agent_events
|
||||
|
||||
src/calliope/agent/harness/log.py:146: OperationalError
|
||||
----------------------------- Captured stderr call -----------------------------
|
||||
2026-10-21 20:00:31,637 | INFO | calliope.harness | Harness composed: 79 tools
|
||||
------------------------------ Captured log call -------------------------------
|
||||
INFO calliope.harness:__init__.py:214 Harness composed: 79 tools
|
||||
___________ test_h3_messages_bind_neighbor_shots_and_keep_file_slots ___________
|
||||
|
||||
def test_h3_messages_bind_neighbor_shots_and_keep_file_slots():
|
||||
from calliope.agent.prompts import build_minimax_h3_ref_messages
|
||||
from calliope.agent.video_agent import resolve_h3_references
|
||||
from calliope.comfyui.parser import parse_dynamic_inputs
|
||||
|
||||
plan = {
|
||||
"overview": {"style": "noir", "lighting": "sodium"},
|
||||
"requirements": {"lighting": "sodium lamps only"},
|
||||
"shots": [
|
||||
{
|
||||
"clip_id": 1,
|
||||
"window": "00:00.000-00:04.000",
|
||||
"action": "enter",
|
||||
"camera": "push in",
|
||||
"lighting": "dim sodium",
|
||||
"speakers": "MAYA: hello",
|
||||
"spatial": "screen left",
|
||||
"state_after": "door open",
|
||||
},
|
||||
{
|
||||
"clip_id": 2,
|
||||
"window": "00:04.000-00:08.000",
|
||||
"action": "turn",
|
||||
"camera": "hold",
|
||||
"lighting": "dim sodium",
|
||||
"speakers": "",
|
||||
"spatial": "screen left",
|
||||
"state_after": "",
|
||||
},
|
||||
{
|
||||
"clip_id": 3,
|
||||
"window": "00:08.000-00:12.000",
|
||||
"action": "leave",
|
||||
"camera": "pull out",
|
||||
"lighting": "brighter",
|
||||
"speakers": "",
|
||||
"spatial": "",
|
||||
"state_after": "hallway empty",
|
||||
},
|
||||
],
|
||||
}
|
||||
lock = continuity_lock_text(plan, 2)
|
||||
assert "Previous shot" in lock
|
||||
assert "Next shot" in lock
|
||||
assert "Do not recast a subject" in lock
|
||||
assert "screen direction" in lock
|
||||
assert "enter" in lock and "leave" in lock
|
||||
|
||||
scene = {"heading": "INT. HALL", "action": "She turns.", "duration_sec": 4, "dialog": ""}
|
||||
subjects = [
|
||||
{
|
||||
"index": 1,
|
||||
"kind": "reference",
|
||||
"name": "mercs",
|
||||
"appearance": "",
|
||||
"path": r"E:\assets\mercs.png",
|
||||
}
|
||||
]
|
||||
messages = build_minimax_h3_ref_messages(scene, subjects, continuity=lock)
|
||||
text = messages[1]["content"]
|
||||
assert "Previous shot" in text
|
||||
assert "Next shot" in text
|
||||
assert "screen direction" in text
|
||||
assert "do not substitute another character or location" in text
|
||||
|
||||
workflow = {
|
||||
"148": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": ""},
|
||||
"_meta": {"title": "Ref 1 (Input:image)"},
|
||||
},
|
||||
"149": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": ""},
|
||||
"_meta": {"title": "Ref 2 (Input:image)"},
|
||||
},
|
||||
}
|
||||
inputs = parse_dynamic_inputs(workflow)
|
||||
resolved, paths, _videos = resolve_h3_references(
|
||||
inputs,
|
||||
{"148": r"E:\assets\hero.png", "149": r"E:\assets\mercs.png"},
|
||||
[{"name": "Maya", "appearance": "olive jacket", "sheet_path": r"E:\assets\maya.png"}],
|
||||
{"name": "Metro Line", "description": "rain-slick rooftops"},
|
||||
r"E:\assets\metro.png",
|
||||
)
|
||||
> assert resolved[1]["name"] == "mercs"
|
||||
E AssertionError: assert 'E:\\assets\\mercs' == 'mercs'
|
||||
E
|
||||
E - mercs
|
||||
E + E:\assets\mercs
|
||||
|
||||
tests/test_continuity.py:249: AssertionError
|
||||
___________ test_resolve_h3_references_prefers_form_files_over_story ___________
|
||||
|
||||
def test_resolve_h3_references_prefers_form_files_over_story():
|
||||
"""Slot files the user picked replace the story cast; empty slots keep it."""
|
||||
from calliope.agent.video_agent import resolve_h3_references
|
||||
from calliope.comfyui.parser import parse_dynamic_inputs
|
||||
|
||||
workflow = {
|
||||
"148": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": ""},
|
||||
"_meta": {"title": "Ref 1 (Input:image)"},
|
||||
},
|
||||
"149": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": ""},
|
||||
"_meta": {"title": "Ref 2 (Input:image)"},
|
||||
},
|
||||
"150": {
|
||||
"class_type": "LoadVideo",
|
||||
"inputs": {"video": ""},
|
||||
"_meta": {"title": "Motion (Input:video)"},
|
||||
},
|
||||
}
|
||||
inputs = parse_dynamic_inputs(workflow)
|
||||
characters = [
|
||||
{
|
||||
"name": "Maya",
|
||||
"appearance": "olive jacket",
|
||||
"sheet_path": r"E:\assets\maya.png",
|
||||
}
|
||||
]
|
||||
location = {"name": "Metro Line", "description": "rain-slick rooftops"}
|
||||
loc_image = r"E:\assets\metro.png"
|
||||
|
||||
empty_subjects, empty_paths, empty_videos = resolve_h3_references(
|
||||
inputs, {}, characters, location, loc_image
|
||||
)
|
||||
assert [s["name"] for s in empty_subjects] == ["Maya", "Metro Line"]
|
||||
assert empty_paths == [r"E:\assets\maya.png", r"E:\assets\metro.png"]
|
||||
assert empty_videos == []
|
||||
|
||||
subjects, paths, videos = resolve_h3_references(
|
||||
inputs,
|
||||
{
|
||||
"148": r"E:\assets\maya.png",
|
||||
"149": r"E:\assets\mercs.png",
|
||||
"150": r"E:\assets\fight.mp4",
|
||||
},
|
||||
characters,
|
||||
location,
|
||||
loc_image,
|
||||
)
|
||||
assert subjects[0]["kind"] == "character"
|
||||
assert subjects[0]["name"] == "Maya"
|
||||
assert subjects[1]["kind"] == "reference"
|
||||
> assert subjects[1]["name"] == "mercs"
|
||||
E AssertionError: assert 'E:\\assets\\mercs' == 'mercs'
|
||||
E
|
||||
E - mercs
|
||||
E + E:\assets\mercs
|
||||
|
||||
tests/test_h3_profile.py:215: AssertionError
|
||||
___________ test_preview_prompt_uses_form_references_not_story_cast ____________
|
||||
|
||||
client = <starlette.testclient.TestClient object at 0x7f3b525d3830>
|
||||
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f3b525d0c80>
|
||||
|
||||
def test_preview_prompt_uses_form_references_not_story_cast(client, monkeypatch):
|
||||
"""The Video-tab image and video slots are the prompt's subjects.
|
||||
|
||||
Story character + location text must not replace files the user picked,
|
||||
and Regenerate (force) must not return a draft written before those files.
|
||||
"""
|
||||
pid = _mk_project(client, "Preview refs")
|
||||
scene = _add_scene(client, pid, 1, action="She swings the extinguisher.")
|
||||
clip_id = _scene_default_clip(pid, scene["id"])
|
||||
|
||||
wf = {
|
||||
"10": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": ""},
|
||||
"_meta": {"title": "Main Prompt (Input:prompt)"},
|
||||
},
|
||||
"148": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": ""},
|
||||
"_meta": {"title": "Ref 1 (Input:image)"},
|
||||
},
|
||||
"149": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": ""},
|
||||
"_meta": {"title": "Ref 2 (Input:image)"},
|
||||
},
|
||||
"150": {
|
||||
"class_type": "LoadVideo",
|
||||
"inputs": {"video": ""},
|
||||
"_meta": {"title": "Motion (Input:video)"},
|
||||
},
|
||||
}
|
||||
conn = get_db(settings.db_path)
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"""
|
||||
INSERT INTO workflows (name, kind, workflow_json, input_schema, output_schema,
|
||||
prompt_profile, is_enabled)
|
||||
VALUES (?, 'video', ?, '[]', '[]', 'minimax_h3_ref', 1)
|
||||
""",
|
||||
("H3 refs", json.dumps(wf)),
|
||||
)
|
||||
wf_id = cur.lastrowid
|
||||
char = conn.execute(
|
||||
"""
|
||||
INSERT INTO characters (project_id, name, appearance, sheet_path)
|
||||
VALUES (?, 'Maya', 'olive jacket', ?)
|
||||
""",
|
||||
(pid, r"E:\story\maya-sheet.png"),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO scene_characters (scene_id, character_id) VALUES (?, ?)",
|
||||
(scene["id"], char.lastrowid),
|
||||
)
|
||||
loc = conn.execute(
|
||||
"""
|
||||
INSERT INTO locations (project_id, name, description, reference_image_path)
|
||||
VALUES (?, 'Metro Line', 'rain-slick rooftops', ?)
|
||||
""",
|
||||
(pid, r"E:\story\metro.png"),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE scenes SET location_id = ? WHERE id = ?",
|
||||
(loc.lastrowid, scene["id"]),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE clips SET workflow_id = ? WHERE id = ?", (wf_id, clip_id)
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
seen: dict = {}
|
||||
|
||||
async def fake_rewrite(scene_, subjects, **kwargs):
|
||||
seen["subjects"] = subjects
|
||||
seen["videos"] = kwargs.get("videos")
|
||||
seen["calls"] = seen.get("calls", 0) + 1
|
||||
return "REWRITE FROM REFS"
|
||||
|
||||
monkeypatch.setattr("calliope.agent.video_agent._h3_rewrite", fake_rewrite)
|
||||
|
||||
form = {
|
||||
"148": r"E:\refs\hero.png",
|
||||
"149": r"E:\refs\mercs.png",
|
||||
"150": r"E:\refs\fight.mp4",
|
||||
}
|
||||
r = client.post(
|
||||
f"/api/jobs/projects/{pid}/preview-prompt",
|
||||
json={"clip_id": clip_id, "input_values": form},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["prompt"] == "REWRITE FROM REFS"
|
||||
assert body["from_draft"] is False
|
||||
names = [s["name"] for s in seen["subjects"]]
|
||||
> assert names == ["hero", "mercs"]
|
||||
E AssertionError: assert ['E:\\refs\\h...\refs\\mercs'] == ['hero', 'mercs']
|
||||
E
|
||||
E At index 0 diff: 'E:\\refs\\hero' != 'hero'
|
||||
E Use -v to get more diff
|
||||
|
||||
tests/test_scene_settings.py:487: AssertionError
|
||||
---------------------------- Captured stderr setup -----------------------------
|
||||
2026-10-21 20:00:31,702 | INFO | calliope | Calliope started — db=/tmp/multica-task-3084682389/tmpj2vinw90/calliope.db dry_run=True
|
||||
2026-10-21 20:00:31,702 | INFO | calliope.worker | Queue worker started
|
||||
------------------------------ Captured log setup ------------------------------
|
||||
INFO calliope:main.py:112 Calliope started — db=/tmp/multica-task-3084682389/tmpj2vinw90/calliope.db dry_run=True
|
||||
INFO calliope.worker:worker.py:79 Queue worker started
|
||||
----------------------------- Captured stderr call -----------------------------
|
||||
2026-10-21 20:00:31,707 | INFO | httpx2 | HTTP Request: POST http://testserver/api/projects "HTTP/1.1 200 OK"
|
||||
2026-10-21 20:00:31,721 | INFO | httpx2 | HTTP Request: POST http://testserver/api/projects/1/scenes "HTTP/1.1 200 OK"
|
||||
2026-10-21 20:00:31,733 | INFO | httpx2 | HTTP Request: POST http://testserver/api/jobs/projects/1/preview-prompt "HTTP/1.1 200 OK"
|
||||
------------------------------ Captured log call -------------------------------
|
||||
INFO httpx2:_client.py:1085 HTTP Request: POST http://testserver/api/projects "HTTP/1.1 200 OK"
|
||||
INFO httpx2:_client.py:1085 HTTP Request: POST http://testserver/api/projects/1/scenes "HTTP/1.1 200 OK"
|
||||
INFO httpx2:_client.py:1085 HTTP Request: POST http://testserver/api/jobs/projects/1/preview-prompt "HTTP/1.1 200 OK"
|
||||
--------------------------- Captured stderr teardown ---------------------------
|
||||
2026-10-21 20:00:32,204 | INFO | calliope.worker | Queue worker stopped
|
||||
2026-10-21 20:00:32,204 | INFO | calliope | Calliope shutting down
|
||||
---------------------------- Captured log teardown -----------------------------
|
||||
INFO calliope.worker:worker.py:185 Queue worker stopped
|
||||
INFO calliope:main.py:118 Calliope shutting down
|
||||
_________________ test_script_regenerate_keeps_expanded_count __________________
|
||||
|
||||
client = <starlette.testclient.TestClient object at 0x7f3b524d76b0>
|
||||
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f3b525d2180>
|
||||
|
||||
def test_script_regenerate_keeps_expanded_count(client, monkeypatch):
|
||||
calls: list[int] = []
|
||||
|
||||
async def fake_structured(messages, temperature=0.7):
|
||||
user = messages[1]["content"]
|
||||
# Parse required count from prompt
|
||||
n = 4
|
||||
for line in user.splitlines():
|
||||
if line.startswith("required_scene_count:"):
|
||||
n = int(line.split(":", 1)[1].strip())
|
||||
break
|
||||
calls.append(n)
|
||||
return {
|
||||
"scenes": [
|
||||
{
|
||||
"order_index": i,
|
||||
"heading": f"SCENE {i}",
|
||||
"action": f"action {i}",
|
||||
"dialog": "",
|
||||
"duration_sec": 5,
|
||||
"character_ids": [],
|
||||
"location_id": None,
|
||||
}
|
||||
for i in range(1, n + 1)
|
||||
]
|
||||
}
|
||||
|
||||
monkeypatch.setattr("calliope.agent.script_agent.generate_structured", fake_structured)
|
||||
|
||||
r = client.post("/api/projects", json={"title": "Expand", "idea": "desert", "target_duration": "30 seconds"})
|
||||
pid = r.json()["id"]
|
||||
|
||||
# Seed 4 scenes via generate
|
||||
r = client.post(f"/api/projects/{pid}/generate-script", json={"replace": True})
|
||||
> assert r.status_code == 200
|
||||
E assert 500 == 200
|
||||
E + where 500 = <Response [500 Internal Server Error]>.status_code
|
||||
|
||||
tests/test_script_scene_count.py:59: AssertionError
|
||||
---------------------------- Captured stderr setup -----------------------------
|
||||
2026-10-21 20:00:32,234 | INFO | calliope | Calliope started — db=/tmp/multica-task-3084682389/tmp2hqlzdiy/calliope.db dry_run=True
|
||||
2026-10-21 20:00:32,234 | INFO | calliope.worker | Queue worker started
|
||||
------------------------------ Captured log setup ------------------------------
|
||||
INFO calliope:main.py:112 Calliope started — db=/tmp/multica-task-3084682389/tmp2hqlzdiy/calliope.db dry_run=True
|
||||
INFO calliope.worker:worker.py:79 Queue worker started
|
||||
----------------------------- Captured stderr call -----------------------------
|
||||
2026-10-21 20:00:32,238 | INFO | httpx2 | HTTP Request: POST http://testserver/api/projects "HTTP/1.1 200 OK"
|
||||
2026-10-21 20:00:32,264 | INFO | calliope.llm | LLM stream request to http://127.0.0.1:11434/v1/chat/completions with model llama3.2
|
||||
2026-10-21 20:00:32,266 | INFO | httpx2 | HTTP Request: POST http://testserver/api/projects/1/generate-script "HTTP/1.1 500 Internal Server Error"
|
||||
------------------------------ Captured log call -------------------------------
|
||||
INFO httpx2:_client.py:1085 HTTP Request: POST http://testserver/api/projects "HTTP/1.1 200 OK"
|
||||
INFO calliope.llm:llm.py:360 LLM stream request to http://127.0.0.1:11434/v1/chat/completions with model llama3.2
|
||||
INFO httpx2:_client.py:1085 HTTP Request: POST http://testserver/api/projects/1/generate-script "HTTP/1.1 500 Internal Server Error"
|
||||
--------------------------- Captured stderr teardown ---------------------------
|
||||
2026-10-21 20:00:32,735 | INFO | calliope.worker | Queue worker stopped
|
||||
2026-10-21 20:00:32,736 | INFO | calliope | Calliope shutting down
|
||||
---------------------------- Captured log teardown -----------------------------
|
||||
INFO calliope.worker:worker.py:185 Queue worker stopped
|
||||
INFO calliope:main.py:118 Calliope shutting down
|
||||
=============================== warnings summary ===============================
|
||||
../../aigc-home/calliope-backend/.venv/lib/python3.12/site-packages/starlette/testclient.py:53
|
||||
/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-728-a07b128af8f7/workdir/aigc-home/calliope-backend/.venv/lib/python3.12/site-packages/starlette/testclient.py:53: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
|
||||
_PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
=========================== short test summary info ============================
|
||||
FAILED tests/test_agent_comfy_mcp.py::test_comfy_server_info_scoping - sqlite...
|
||||
FAILED tests/test_continuity.py::test_h3_messages_bind_neighbor_shots_and_keep_file_slots
|
||||
FAILED tests/test_h3_profile.py::test_resolve_h3_references_prefers_form_files_over_story
|
||||
FAILED tests/test_scene_settings.py::test_preview_prompt_uses_form_references_not_story_cast
|
||||
FAILED tests/test_script_scene_count.py::test_script_regenerate_keeps_expanded_count
|
||||
5 failed, 1 warning in 1.19s
|
||||
@@ -0,0 +1,33 @@
|
||||
# HEL-728 B0 版本核实与回退记录
|
||||
|
||||
核实日期:2026-10-21。服务器仅执行只读查询;未部署、重启服务或修改配置/数据。
|
||||
|
||||
| 对象 | 精确版本 / 结论 |
|
||||
| --- | --- |
|
||||
| `origin/main` | `1fc8bdb8bb2a88ccace6f21a507fd77bcf22f532` |
|
||||
| `origin/deploy/hel-691` | `2b3bbf9`,是第五版的祖先,不是当前部署版本 |
|
||||
| `origin/feat/hel-665-refract`、PR #1 head | `35fd07af24e62add1f96f6a9c38e363f8e23d778` |
|
||||
| 当前测试站 | `DEPLOYED_REVISION` 标为第五版 `35fd07a`;前端 191 文件、后端 79 个 Python 文件逐项 SHA-256 全匹配该提交 |
|
||||
| 静态构建 | 当前 static 与 `stage-HEL665-v5-35fd07a-20261015/build` 全目录 `diff -qr` 无差异;两份 index.html 的 SHA-256 均为 `acd3ba6e0ab29b92f5133358398f3544baa0ce2bea03560004a42e812b6abc8a` |
|
||||
| PR #1 | https://gitea.xbay.cc/leefer/aigc-home/pulls/1 :open、未合并、目标 main、head=35fd07a |
|
||||
| B0 底稿 | 从完整第五版另开 `feat/hel-728-night-foundation`,没有合并旧 PR 或更改 main |
|
||||
|
||||
第五版领先 main 22 个提交、领先 deploy/hel-691 10 个提交。HEL-692、HEL-695 的实装和后续修复均在该祖先链上:从 aaddd3d 到 35fd07a 的创作草稿/端点归属、手动播放、智能体编辑成果、阶段建议与确认隔离保留。第五版也完整包含 HEL-681/682 批量导入和 HEL-684 生成结果回流。
|
||||
|
||||
HEL-727 把两个 API 返回 422 作为部署版本依据的结论已由源码/发布包实证纠正。当前差异均已解释,没有以旧 deploy 分支覆盖后续工作。
|
||||
|
||||
本任务专用检出初始 `git status --short` 为空,无未提交/未推送工作被覆盖。服务器应用目录不是 Git 检出,以全部源码哈希替代 git status 核实,没有发现与第五版不同的产品源码。已有 `.before-*` 和发布备份保留。未触碰其他任务私有工作区。
|
||||
|
||||
本地保全引用:`backup/hel-728-main-1fc8bdb` 与 `backup/hel-728-v5-35fd07a`。完整证据见同目录 `server-audit.txt`、`server-backend.txt`、`backup-hashes.txt`。
|
||||
|
||||
## 已存在的服务器备份
|
||||
|
||||
目录 `/opt/aigc-home/releases`:
|
||||
|
||||
- `formal-v5-35fd07a.tgz`:当前第五版发布包。
|
||||
- `backup-HEL665-v5-35fd07a-20261015.tgz`:部署第五版之前的静态页、前端源码、workbench 路由及版本标记。
|
||||
- `rollback-HEL665-v5-35fd07a-20261015.sh`:第五版回退脚本,旧源码/静态目录均仍存在。**此脚本会回到第五版之前,不能当成本批将来的回退脚本。**
|
||||
|
||||
备份和脚本哈希已留档。本批尚未部署,因此当前站点无需回退。后续由总工部署 B0 时,须先重新核对当前源码/构建哈希及活动任务,并备份当前第五版,才能替换 B0 静态页、前端源码和新增的 app.html/themes.css。B0 回退应恢复这份第五版完整备份;无需数据库回退。本批无后端、数据迁移或用户配置改动。
|
||||
|
||||
代码回退:以 `35fd07af24e62add1f96f6a9c38e363f8e23d778` 为基线重新构建第五版,或恢复总工部署前生成的完整备份。不要只切回 `deploy/hel-691`。
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"checks": [
|
||||
"默认主题为夜片场",
|
||||
"night 保留原编辑组件和正反向草稿",
|
||||
"day 保留原编辑组件和正反向草稿",
|
||||
"grade 保留原编辑组件和正反向草稿",
|
||||
"bluehour 保留原编辑组件和正反向草稿",
|
||||
"刷新恢复主题和草稿",
|
||||
"方向键移动主题选项焦点",
|
||||
"Esc 关闭并恢复入口焦点",
|
||||
"创作台兼容入口保留全部参数及片段",
|
||||
"隔离真实队列已收到任务",
|
||||
"night 上传素材和运行中编辑状态保留",
|
||||
"day 上传素材和运行中编辑状态保留",
|
||||
"grade 上传素材和运行中编辑状态保留",
|
||||
"bluehour 上传素材和运行中编辑状态保留",
|
||||
"切主题后原任务完成且产物归属不变",
|
||||
"切主题保留视频节点和暂停状态",
|
||||
"切主题保留智能体任务输入组件和草稿",
|
||||
"切主题保留未保存设置",
|
||||
"1440,900 night 导航入口和四主题完整可见",
|
||||
"1440,900 day 导航入口和四主题完整可见",
|
||||
"1440,900 grade 导航入口和四主题完整可见",
|
||||
"1440,900 bluehour 导航入口和四主题完整可见",
|
||||
"1366,768 night 导航入口和四主题完整可见",
|
||||
"1366,768 day 导航入口和四主题完整可见",
|
||||
"1366,768 grade 导航入口和四主题完整可见",
|
||||
"1366,768 bluehour 导航入口和四主题完整可见",
|
||||
"1280,720 night 导航入口和四主题完整可见",
|
||||
"1280,720 day 导航入口和四主题完整可见",
|
||||
"1280,720 grade 导航入口和四主题完整可见",
|
||||
"1280,720 bluehour 导航入口和四主题完整可见",
|
||||
"模型连接入口保留页签参数",
|
||||
"旧项目链接保留阶段参数",
|
||||
"全局搜索保留 视频创作",
|
||||
"全局搜索保留 大语言模型",
|
||||
"全局搜索保留 试验台",
|
||||
"全局搜索保留 三维搭景",
|
||||
"night 首次绘制前恢复主题及底色",
|
||||
"day 首次绘制前恢复主题及底色",
|
||||
"grade 首次绘制前恢复主题及底色",
|
||||
"bluehour 首次绘制前恢复主题及底色",
|
||||
"存储被禁用仍可切换且显示提示",
|
||||
"存储被禁用保留输入",
|
||||
"无效存储回退夜片场",
|
||||
"浏览器无运行错误"
|
||||
],
|
||||
"errors": []
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
|
||||
> calliope-web@1.5.8 build
|
||||
> vite build
|
||||
|
||||
vite v6.4.3 building SSR bundle for production...
|
||||
transforming...
|
||||
8:10:50 PM [vite-plugin-svelte] src/lib/components/workbench/ShotStagePanel.svelte:156:5 `<video>` elements must have a `<track kind="captions">`
|
||||
https://svelte.dev/e/a11y_media_has_caption
|
||||
154: {#if shot.description}<p class="vshot-desc">{shot.description}</p>{/if}
|
||||
155: {#if shot.job?.status === 'done' && shot.job.output}
|
||||
156: <video
|
||||
^
|
||||
157: class="vshot-video"
|
||||
158: src={assetUrl(shot.job.output) ?? undefined}
|
||||
"handleConnectionChange" and "handleAttributionWarning" are imported from external module "@xyflow/system" but never used in "node_modules/@xyflow/svelte/dist/lib/hooks/useNodeConnections.svelte.js", "node_modules/@xyflow/svelte/dist/lib/components/Handle/Handle.svelte" and "node_modules/@xyflow/svelte/dist/lib/components/Attribution/Attribution.svelte".
|
||||
✓ 533 modules transformed.
|
||||
rendering chunks...
|
||||
vite v6.4.3 building for production...
|
||||
transforming...
|
||||
8:10:53 PM [vite-plugin-svelte] src/lib/components/workbench/ShotStagePanel.svelte:156:5 `<video>` elements must have a `<track kind="captions">`
|
||||
https://svelte.dev/e/a11y_media_has_caption
|
||||
154: {#if shot.description}<p class="vshot-desc">{shot.description}</p>{/if}
|
||||
155: {#if shot.job?.status === 'done' && shot.job.output}
|
||||
156: <video
|
||||
^
|
||||
157: class="vshot-video"
|
||||
158: src={assetUrl(shot.job.output) ?? undefined}
|
||||
✓ 778 modules transformed.
|
||||
rendering chunks...
|
||||
computing gzip size...
|
||||
.svelte-kit/output/client/_app/version.json 0.03 kB │ gzip: 0.05 kB
|
||||
.svelte-kit/output/client/.vite/manifest.json 28.72 kB │ gzip: 3.35 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/reduceMotion.BrXLPzfy.css 0.08 kB │ gzip: 0.10 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/14.IyyrLGyK.css 0.22 kB │ gzip: 0.15 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/6.C62bpzl4.css 0.22 kB │ gzip: 0.15 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/Skeleton.BQeWIsLh.css 0.39 kB │ gzip: 0.22 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/SafeMedia.Cf_dgjFv.css 0.51 kB │ gzip: 0.31 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/EmptyState.BXjpyO2u.css 0.58 kB │ gzip: 0.31 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/Spinner.us8fwMZC.css 0.73 kB │ gzip: 0.37 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/ConfirmDialog.DOfiQL0c.css 0.91 kB │ gzip: 0.38 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/ProgressBar.DG8dH-U_.css 1.44 kB │ gzip: 0.58 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/22.ChQ8T8dK.css 1.50 kB │ gzip: 0.56 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/20.D7w-Sx1Y.css 1.66 kB │ gzip: 0.55 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/AgentPlanPanel.BMGRQZdH.css 2.24 kB │ gzip: 0.74 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/ImageLightbox.DN4QbnbA.css 2.55 kB │ gzip: 0.76 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/types.DUMMEwmy.css 4.86 kB │ gzip: 1.13 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/17.CdHWgDYY.css 4.90 kB │ gzip: 1.33 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/AgentComposer.BhJ_F-aX.css 4.90 kB │ gzip: 1.31 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/18.D3s-Wa9z.css 5.02 kB │ gzip: 1.31 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/WorkflowBay.CrZOCTCD.css 5.20 kB │ gzip: 1.40 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/AProjectCard.BhK9fW6H.css 5.39 kB │ gzip: 1.47 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/11.CpN-OyyL.css 5.63 kB │ gzip: 1.45 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/8.BK-8H9u5.css 9.05 kB │ gzip: 2.13 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/3.CxKDlY5x.css 9.73 kB │ gzip: 2.04 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/AgentChat.C5wVAvmg.css 11.44 kB │ gzip: 2.53 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/13.DdkN80tf.css 11.76 kB │ gzip: 2.55 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/AppShell.Bk5S4WfJ.css 14.10 kB │ gzip: 3.27 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/5.DoWQHLuE.css 15.17 kB │ gzip: 3.13 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/0.C2N0LW81.css 19.69 kB │ gzip: 3.99 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/WorkflowLibrary.CN-_i5Sc.css 27.67 kB │ gzip: 4.81 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/4.BQ063dE_.css 29.80 kB │ gzip: 5.74 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/7.B1E9ZlPG.css 29.94 kB │ gzip: 5.07 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/21.BY02oOwl.css 30.01 kB │ gzip: 4.94 kB
|
||||
.svelte-kit/output/client/_app/immutable/assets/StudioDesk.DiLnYEIx.css 31.15 kB │ gzip: 6.17 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/5.hx3kxUVa.js 0.09 kB │ gzip: 0.10 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/Bzak7iHL.js 0.10 kB │ gzip: 0.10 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/IKQcdn9t.js 0.12 kB │ gzip: 0.12 kB
|
||||
.svelte-kit/output/client/_app/immutable/entry/start.BUwQ6lIJ.js 0.12 kB │ gzip: 0.11 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/B0XwC4Ot.js 0.15 kB │ gzip: 0.14 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/BsCRBkET.js 0.18 kB │ gzip: 0.16 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/Sbfne8nC.js 0.19 kB │ gzip: 0.17 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/FwfFJvU_.js 0.19 kB │ gzip: 0.16 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/DEfCzHLx.js 0.23 kB │ gzip: 0.18 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/Ba1assjq.js 0.26 kB │ gzip: 0.22 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/16.sWMaj3Bi.js 0.26 kB │ gzip: 0.23 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/BXFzAH0P.js 0.28 kB │ gzip: 0.18 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/15.DYsaZg84.js 0.29 kB │ gzip: 0.25 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/CK7WZZfM.js 0.31 kB │ gzip: 0.18 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/12.CF9Zu6pN.js 0.33 kB │ gzip: 0.27 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/9.BVfZlOMu.js 0.34 kB │ gzip: 0.28 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/2.BDtoJguU.js 0.37 kB │ gzip: 0.25 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/BWaKaORA.js 0.37 kB │ gzip: 0.26 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/1.Bm5c1i2N.js 0.40 kB │ gzip: 0.28 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/10.DPw0t24L.js 0.41 kB │ gzip: 0.26 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/19.EN81mzhv.js 0.41 kB │ gzip: 0.26 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/6n2EhuN1.js 0.46 kB │ gzip: 0.30 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/DNKIhv1n.js 0.47 kB │ gzip: 0.34 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/BpBwGJjl.js 0.49 kB │ gzip: 0.34 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/-UyI9lYi.js 0.51 kB │ gzip: 0.30 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/D41pZ3kD.js 0.56 kB │ gzip: 0.32 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/5hKhZLI5.js 0.62 kB │ gzip: 0.36 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/BXakyGcx.js 0.71 kB │ gzip: 0.43 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/8OLyFVGd.js 0.75 kB │ gzip: 0.42 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/CN9eJxB2.js 0.76 kB │ gzip: 0.42 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/CNhvvmZJ.js 0.83 kB │ gzip: 0.42 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/DCtQckrV.js 1.09 kB │ gzip: 0.62 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/14.CvNq-suX.js 1.09 kB │ gzip: 0.66 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/6.BpRKot_7.js 1.20 kB │ gzip: 0.71 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/Dp1pzeXC.js 1.27 kB │ gzip: 0.73 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/wbPk3Yxo.js 1.37 kB │ gzip: 0.66 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/D3mdsp0g.js 1.43 kB │ gzip: 0.66 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/PEmPcvxY.js 1.49 kB │ gzip: 0.78 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/whH4D4QZ.js 1.49 kB │ gzip: 0.75 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/LuZC9yWf.js 1.99 kB │ gzip: 1.05 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/t712vir_.js 2.24 kB │ gzip: 0.99 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/20.COu3iy7d.js 2.37 kB │ gzip: 1.38 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/C5fjXKkg.js 2.47 kB │ gzip: 1.12 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/BPNbeFLK.js 2.77 kB │ gzip: 1.28 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/22.C5Nwx_zE.js 2.92 kB │ gzip: 1.61 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/CnYh2b86.js 2.95 kB │ gzip: 1.24 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/Bie-hupT.js 3.08 kB │ gzip: 1.57 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/DK7AMg8o.js 3.23 kB │ gzip: 1.55 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/DDlx89yg.js 4.27 kB │ gzip: 1.51 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/C1VtnVx0.js 6.86 kB │ gzip: 3.09 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/BFgrkAWo.js 8.09 kB │ gzip: 3.27 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/PWQRmEWD.js 10.09 kB │ gzip: 2.31 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/18.Dqn9lNj5.js 10.20 kB │ gzip: 3.92 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/CELneDUs.js 10.41 kB │ gzip: 4.44 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/0.B9UHNpdV.js 11.98 kB │ gzip: 4.40 kB
|
||||
.svelte-kit/output/client/_app/immutable/entry/app.BgS0U9Aq.js 12.58 kB │ gzip: 4.11 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/StFt4UCs.js 14.41 kB │ gzip: 5.54 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/11.CYMf0Aop.js 17.10 kB │ gzip: 6.44 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/CHDY03l9.js 17.32 kB │ gzip: 6.96 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/CI5J-U9v.js 17.44 kB │ gzip: 6.33 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/3.lt50pTEv.js 19.56 kB │ gzip: 6.87 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/Cj6gCTdL.js 20.25 kB │ gzip: 7.21 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/8.c25p5xbs.js 21.27 kB │ gzip: 7.28 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/17.C8PkQczY.js 23.50 kB │ gzip: 7.73 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/Dmrd7xQR.js 23.98 kB │ gzip: 9.32 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/13.CGct_Wju.js 24.87 kB │ gzip: 8.36 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/CKnAzW_q.js 36.67 kB │ gzip: 14.42 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/BwCNePTm.js 41.30 kB │ gzip: 15.68 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/BR687OLR.js 49.24 kB │ gzip: 17.84 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/4.C6iXVhZu.js 50.59 kB │ gzip: 17.70 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/21.Cz9clG7U.js 54.65 kB │ gzip: 15.14 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/Bmp6xnce.js 71.50 kB │ gzip: 26.91 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/C5t8Nts6.js 87.12 kB │ gzip: 27.47 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/Bvurq_58.js 92.46 kB │ gzip: 32.40 kB
|
||||
.svelte-kit/output/client/_app/immutable/nodes/7.D95zSIUA.js 211.51 kB │ gzip: 68.00 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/BQUFPnBP.js 684.72 kB │ gzip: 180.05 kB
|
||||
.svelte-kit/output/client/_app/immutable/chunks/vMwlJ6hm.js 774.61 kB │ gzip: 226.22 kB
|
||||
|
||||
(!) Some chunks are larger than 500 kB after minification. Consider:
|
||||
- Using dynamic import() to code-split the application
|
||||
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
|
||||
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
|
||||
✓ built in 3.57s
|
||||
.svelte-kit/output/server/.vite/manifest.json 25.84 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/Icon.BrXLPzfy.css 0.08 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.C62bpzl4.css 0.22 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.IyyrLGyK.css 0.22 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/Skeleton.BQeWIsLh.css 0.39 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/SafeMedia.Cf_dgjFv.css 0.51 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/EmptyState.BXjpyO2u.css 0.58 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/Spinner.us8fwMZC.css 0.73 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/ConfirmDialog.C_pBcjz1.css 0.89 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/ProgressBar.DG8dH-U_.css 1.44 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.ChQ8T8dK.css 1.50 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.D7w-Sx1Y.css 1.66 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/AgentPlanPanel.BMGRQZdH.css 2.24 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/ImageLightbox.DhBJATfN.css 2.53 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.Bn3DGoQS.css 3.55 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/AgentComposer.JZyE_n9K.css 4.85 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/RefImageGrid.DUMMEwmy.css 4.86 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.D3s-Wa9z.css 5.02 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/WorkflowBay.CrZOCTCD.css 5.20 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/AProjectCard.BHJv_kAx.css 5.30 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.CpN-OyyL.css 5.63 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.u1ByS4kk.css 9.02 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.k-tm080m.css 9.35 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.CxKDlY5x.css 9.73 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/AgentChat.DGaLhJTa.css 11.41 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.CHFzkBke.css 11.66 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/AppShell.q20u3EwN.css 14.02 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_layout.MRWQDt5E.css 19.46 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/WorkflowLibrary.QeU6Pxlx.css 27.65 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.BPi-Y6Y8.css 29.59 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.UfnkmHg7.css 29.89 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/_page.BY02oOwl.css 30.01 kB
|
||||
.svelte-kit/output/server/_app/immutable/assets/StudioDesk.y8URPnQJ.css 31.03 kB
|
||||
.svelte-kit/output/server/entries/pages/build-scene/_page.ts.js 0.04 kB
|
||||
.svelte-kit/output/server/entries/pages/canvas/_page.ts.js 0.04 kB
|
||||
.svelte-kit/output/server/entries/pages/canvas/_id_/_page.ts.js 0.04 kB
|
||||
.svelte-kit/output/server/entries/pages/playground/_page.ts.js 0.04 kB
|
||||
.svelte-kit/output/server/entries/pages/workflows/_page.ts.js 0.04 kB
|
||||
.svelte-kit/output/server/entries/pages/_layout.ts.js 0.06 kB
|
||||
.svelte-kit/output/server/entries/pages/agents/_page.ts.js 0.08 kB
|
||||
.svelte-kit/output/server/entries/pages/library/_page.ts.js 0.08 kB
|
||||
.svelte-kit/output/server/entries/pages/models/_page.ts.js 0.08 kB
|
||||
.svelte-kit/output/server/entries/pages/settings/_page.ts.js 0.08 kB
|
||||
.svelte-kit/output/server/entries/pages/workbench/_page.ts.js 0.08 kB
|
||||
.svelte-kit/output/server/entries/pages/workbench/_id_/_page.ts.js 0.08 kB
|
||||
.svelte-kit/output/server/entries/pages/_page.ts.js 0.12 kB
|
||||
.svelte-kit/output/server/entries/hooks.server.js 0.13 kB
|
||||
.svelte-kit/output/server/entries/pages/projects/_page.ts.js 0.21 kB
|
||||
.svelte-kit/output/server/chunks/index-server.js 0.22 kB
|
||||
.svelte-kit/output/server/env.js 0.23 kB
|
||||
.svelte-kit/output/server/entries/pages/project/_id_/_page.ts.js 0.26 kB
|
||||
.svelte-kit/output/server/entries/pages/create/_page.ts.js 0.30 kB
|
||||
.svelte-kit/output/server/chunks/client.js 0.31 kB
|
||||
.svelte-kit/output/server/entries/pages/model-connections/_page.ts.js 0.31 kB
|
||||
.svelte-kit/output/server/internal.js 0.35 kB
|
||||
.svelte-kit/output/server/entries/fallbacks/error.svelte.js 0.35 kB
|
||||
.svelte-kit/output/server/chunks/state.svelte.js 0.45 kB
|
||||
.svelte-kit/output/server/chunks/Skeleton.js 0.48 kB
|
||||
.svelte-kit/output/server/chunks/Spinner.js 0.52 kB
|
||||
.svelte-kit/output/server/entries/pages/canvas/_id_/_layout.svelte.js 0.55 kB
|
||||
.svelte-kit/output/server/entries/pages/image/_page.svelte.js 0.55 kB
|
||||
.svelte-kit/output/server/entries/pages/video/_page.svelte.js 0.55 kB
|
||||
.svelte-kit/output/server/chunks/index2.js 0.57 kB
|
||||
.svelte-kit/output/server/chunks/server.js 0.62 kB
|
||||
.svelte-kit/output/server/entries/pages/canvas/_page.svelte.js 0.66 kB
|
||||
.svelte-kit/output/server/entries/pages/playground/_page.svelte.js 0.67 kB
|
||||
.svelte-kit/output/server/chunks/utils2.js 0.72 kB
|
||||
.svelte-kit/output/server/chunks/ids.js 0.77 kB
|
||||
.svelte-kit/output/server/chunks/Icon.svelte_svelte_type_style_lang.js 0.86 kB
|
||||
.svelte-kit/output/server/chunks/endpointKind.js 0.95 kB
|
||||
.svelte-kit/output/server/chunks/createMutation.js 1.02 kB
|
||||
.svelte-kit/output/server/chunks/EmptyState.js 1.10 kB
|
||||
.svelte-kit/output/server/chunks/toast.js 1.14 kB
|
||||
.svelte-kit/output/server/chunks/SafeMedia.js 1.65 kB
|
||||
.svelte-kit/output/server/chunks/ProgressBar.js 1.93 kB
|
||||
.svelte-kit/output/server/chunks/useUpload.svelte.js 2.15 kB
|
||||
.svelte-kit/output/server/chunks/context.js 2.27 kB
|
||||
.svelte-kit/output/server/chunks/clone.js 2.33 kB
|
||||
.svelte-kit/output/server/chunks/takeDelete.svelte.js 2.56 kB
|
||||
.svelte-kit/output/server/chunks/index3.js 2.77 kB
|
||||
.svelte-kit/output/server/entries/pages/workbench/_page.svelte.js 2.82 kB
|
||||
.svelte-kit/output/server/entries/pages/_layout.svelte.js 3.08 kB
|
||||
.svelte-kit/output/server/chunks/ConfirmDialog.js 3.25 kB
|
||||
.svelte-kit/output/server/entries/pages/workflows/_page.svelte.js 4.10 kB
|
||||
.svelte-kit/output/server/chunks/internal.js 4.19 kB
|
||||
.svelte-kit/output/server/chunks/exports.js 5.54 kB
|
||||
.svelte-kit/output/server/chunks/Icon.js 6.16 kB
|
||||
.svelte-kit/output/server/entries/pages/agents/_page.svelte.js 8.49 kB
|
||||
.svelte-kit/output/server/chunks/AProjectCard.js 10.17 kB
|
||||
.svelte-kit/output/server/entries/pages/tasks/_page.svelte.js 13.41 kB
|
||||
.svelte-kit/output/server/entries/pages/settings/_page.svelte.js 15.31 kB
|
||||
.svelte-kit/output/server/entries/pages/_page.svelte.js 18.29 kB
|
||||
.svelte-kit/output/server/entries/pages/library/_page.svelte.js 20.35 kB
|
||||
.svelte-kit/output/server/chunks/AppShell.svelte_svelte_type_style_lang.js 20.49 kB
|
||||
.svelte-kit/output/server/chunks/WorkflowBay.js 20.80 kB
|
||||
.svelte-kit/output/server/chunks/RefImageGrid.js 26.16 kB
|
||||
.svelte-kit/output/server/chunks/root.js 29.42 kB
|
||||
.svelte-kit/output/server/chunks/AgentComposer.js 31.59 kB
|
||||
.svelte-kit/output/server/chunks/utils.js 34.14 kB
|
||||
.svelte-kit/output/server/entries/pages/models/_page.svelte.js 36.45 kB
|
||||
.svelte-kit/output/server/remote-entry.js 36.81 kB
|
||||
.svelte-kit/output/server/chunks/AppShell.js 39.08 kB
|
||||
.svelte-kit/output/server/entries/pages/build-scene/_page.svelte.js 51.56 kB
|
||||
.svelte-kit/output/server/entries/pages/workbench/_id_/_page.svelte.js 52.95 kB
|
||||
.svelte-kit/output/server/chunks/index.js 101.49 kB
|
||||
.svelte-kit/output/server/chunks/StudioDesk.js 113.69 kB
|
||||
.svelte-kit/output/server/index.js 130.14 kB
|
||||
.svelte-kit/output/server/entries/pages/canvas/_id_/_page.svelte.js 144.05 kB
|
||||
.svelte-kit/output/server/entries/pages/connections/_page.svelte.js 150.23 kB
|
||||
.svelte-kit/output/server/chunks/i18n.svelte.js 846.64 kB
|
||||
✓ built in 6.69s
|
||||
|
||||
Run npm run preview to preview your production build locally.
|
||||
|
||||
> Using @sveltejs/adapter-static
|
||||
Wrote site to "build"
|
||||
✔ done
|
||||
@@ -0,0 +1,2 @@
|
||||
Checking formatting...
|
||||
All matched files use Prettier code style!
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
> calliope-web@1.5.8 check
|
||||
> svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
|
||||
|
||||
Loading svelte-check in workspace: /home/xiaomo/multica_workspaces/hello-af23464d222e/hel-728-a07b128af8f7/workdir/aigc-home/calliope-web
|
||||
Getting Svelte diagnostics...
|
||||
|
||||
/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-728-a07b128af8f7/workdir/aigc-home/calliope-web/src/lib/components/workbench/ShotStagePanel.svelte:156:6
|
||||
Warn: `<video>` elements must have a `<track kind="captions">`
|
||||
https://svelte.dev/e/a11y_media_has_caption (svelte)
|
||||
{#if shot.job?.status === 'done' && shot.job.output}
|
||||
<video
|
||||
class="vshot-video"
|
||||
src={assetUrl(shot.job.output) ?? undefined}
|
||||
controls
|
||||
preload="metadata"
|
||||
></video>
|
||||
{/if}
|
||||
|
||||
====================================
|
||||
svelte-check found 0 errors and 1 warning in 1 file
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
Oops! Something went wrong! :(
|
||||
|
||||
ESLint: 9.39.5
|
||||
|
||||
ESLint couldn't find an eslint.config.(js|mjs|cjs) file.
|
||||
|
||||
From ESLint v9.0.0, the default configuration file is now eslint.config.js.
|
||||
If you are using a .eslintrc.* file, please follow the migration guide
|
||||
to update your configuration file to the new format:
|
||||
|
||||
https://eslint.org/docs/latest/use/configure/migration-guide
|
||||
|
||||
If you still have problems after following the migration guide, please stop by
|
||||
https://eslint.org/chat/help to chat with the team.
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
> calliope-web@1.5.8 test
|
||||
> node scripts/check-storage-ssr.mjs && node scripts/check-random-uuid.mjs && node scripts/check-skill-frontmatter.mjs && node scripts/check-export-frame.mjs
|
||||
|
||||
ok method-less localStorage (Node SSR shape) degrades to null without throwing
|
||||
ok absent localStorage degrades to null
|
||||
ok throwing localStorage access degrades to null
|
||||
ok setItem failure is swallowed
|
||||
ok working storage round-trips and removes
|
||||
ok no unsafe `typeof localStorage` guards in src/
|
||||
|
||||
storage SSR checks passed.
|
||||
ok insecure-context crypto (getRandomValues only) yields unique v4 UUIDs
|
||||
ok method-less crypto degrades to unique best-effort ids without throwing
|
||||
ok absent crypto degrades to unique best-effort ids without throwing
|
||||
ok native randomUUID preferred when available
|
||||
ok no direct `crypto.randomUUID()` calls in src/
|
||||
|
||||
randomId insecure-context checks passed.
|
||||
ok edit load strips the frontmatter block
|
||||
ok edit load keeps the body
|
||||
ok strip boundary is exact
|
||||
ok strip is idempotent
|
||||
ok second edit-save-reload adds no layer
|
||||
ok plain body unchanged
|
||||
ok page imports the shared strip helper
|
||||
ok no \A-anchored strip regex left behind
|
||||
skill frontmatter checks passed.
|
||||
ok first try is ~1 frame before the end, never the end itself
|
||||
ok later tries step strictly further back
|
||||
ok tries stay within ~1s of the end
|
||||
ok short clip clamps to 0 without duplicates
|
||||
ok unknown / zero duration falls back to t=0
|
||||
ok base name from /api/file?path=…
|
||||
ok base name from a Windows path
|
||||
ok base name from a plain URL
|
||||
ok unsafe characters are replaced
|
||||
ok blob URL without a name falls back to 'video'
|
||||
ok PNG name names the video, the tag and the frame time
|
||||
ok spaces in the tag become dashes
|
||||
ok fully transparent sample → empty
|
||||
ok pure black → black
|
||||
ok near-black encoder noise (16..18) → black
|
||||
ok mid-grey picture → not blank
|
||||
ok dim but visible picture (≈50) → not blank
|
||||
ok dark night scene with a bright highlight → not blank
|
||||
ok 404 → not-found
|
||||
ok 403 → forbidden
|
||||
ok 500 → network
|
||||
ok 206 / 200 are not errors
|
||||
export-frame checks passed.
|
||||
@@ -0,0 +1,13 @@
|
||||
# HEL-728 B0 实现说明
|
||||
|
||||
以 HEL-716/726 确认附件和 HEL-727 模块边界为依据,只建设主题、导航与兼容基础;完整业务页面重排和动效分别留在 B1–B5。
|
||||
|
||||
- `calliope-web/static/themes.css` 是四套语义颜色、字体、尺寸、圆角和特效 token 的唯一产品定义来源。取值直接来自 HEL-726 HTML;现有组件变量在此映射到语义变量,没有另设配色。作品控制、灯箱的固定深色保留。`routes/app.css` 保留公共组件与动效规则,已有金色/紫色按钮和光栅改为消费 token。
|
||||
- `src/app.html` 在绘制前验证 `aigc-theme`,失败回退夜片场;同步加载阻塞渲染的 themes.css,框架代码尚未执行时也有正确底色。`lib/theme.ts` 只改根元素属性,不改路由、不创建 QueryClient、不重建编辑器。写存储失败时继续本次切换,并明确提示;跨标签页存储事件同步主题。
|
||||
- `ThemeChoices.svelte` 在侧栏统一 Modal 和设置外观区复用,含四主题实时 token 预览、中文名/用途、方向键/Enter 操作。Esc/焦点返回复用既有 Modal。侧栏与主题选择适配三档桌面;减少动效沿用现有全局机制。
|
||||
- `shell/nav.ts` 为外壳和全局搜索提供单一导航定义。工作室/项目/创作台/智能体/画布/素材库,以及任务/工作流/模型连接按确认顺序组织。模型页与 ComfyUI 页面保留可见切换链接,原视频、试验台、三维搭景仍可从全局搜索到达。
|
||||
- `/create?kind=video`、`/model-connections?tab=llm` 是 B0 兼容入口,分别转到已有视频/LLM 页面;默认转到图片/ComfyUI 页面。后续页面未实现前不转向空页。原 `/image`、`/video`、`/connections`、`/models`、`/playground`、`/build-scene` 保持现有功能。
|
||||
- 修复既有 `/projects`、`/project/[id]` 的跳转参数丢失;新入口和旧项目入口保留查询及片段,项目来源、工作流、模式、提示词等不改写。设置的旧页签转向也保留原参数。
|
||||
- `design/nocturne` 保存已确认的 v1/v2 规范与 HTML,仅清除行尾空白;均为设计演示,未作为产品运行代码或假生成结果使用。
|
||||
|
||||
B0 没有新增依赖、生成接口、主题测试副本、数据库变更、密钥或线上配置变更。已有页面局部硬编码视觉的全站替换与最终特效验收仍属于 B5,本批没有宣称完成全站重设计。
|
||||
@@ -0,0 +1,48 @@
|
||||
{"image_wf": 71, "video_wf": 72, "endpoint": "1c758e36-793b-4c11-8e08-c139bba68b02"}
|
||||
{'dual': 3, 'h3': 4, 'project': 6}
|
||||
RUN e2e/reference/check.cjs
|
||||
PASS 真实工作流显示正反向左右分栏
|
||||
PASS 工作流列表可应对超过 48 项
|
||||
PASS 列表搜索过滤真实元数据
|
||||
PASS 置顶不切换工作流、不清空草稿
|
||||
PASS 参数取消不改当前值
|
||||
PASS 参数保存应用到当前工作流
|
||||
PASS 真实生成请求提交且取得结果
|
||||
PASS 正反向与种子写入任务
|
||||
locator.click: Timeout 12000ms exceeded.
|
||||
Call log:
|
||||
[2m - waiting for locator('.result-item').first()[22m
|
||||
|
||||
at /home/xiaomo/multica_workspaces/hello-af23464d222e/hel-728-a07b128af8f7/workdir/aigc-home/e2e/reference/check.cjs:146:45 {
|
||||
log: [ " - waiting for locator('.result-item').first()" ],
|
||||
name: 'TimeoutError'
|
||||
}
|
||||
RUN e2e/reference/check-copy-fallback.cjs
|
||||
locator.waitFor: Timeout 15000ms exceeded.
|
||||
Call log:
|
||||
[2m - waiting for getByRole('button', { name: /编辑内容并带回项目/ }) to be visible[22m
|
||||
|
||||
at /home/xiaomo/multica_workspaces/hello-af23464d222e/hel-728-a07b128af8f7/workdir/aigc-home/e2e/reference/check-copy-fallback.cjs:44:54 {
|
||||
log: [
|
||||
" - waiting for getByRole('button', { name: /编辑内容并带回项目/ }) to be visible"
|
||||
],
|
||||
name: 'TimeoutError'
|
||||
}
|
||||
RUN e2e/hel728.cjs
|
||||
PASS 默认主题为夜片场
|
||||
PASS night 保留原编辑组件和正反向草稿
|
||||
PASS day 保留原编辑组件和正反向草稿
|
||||
PASS grade 保留原编辑组件和正反向草稿
|
||||
PASS bluehour 保留原编辑组件和正反向草稿
|
||||
PASS 刷新恢复主题和草稿
|
||||
PASS 方向键移动主题选项焦点
|
||||
PASS Esc 关闭并恢复入口焦点
|
||||
locator.waitFor: Timeout 30000ms exceeded.
|
||||
Call log:
|
||||
[2m - waiting for locator('#desk-prompt') to be visible[22m
|
||||
|
||||
at /home/xiaomo/multica_workspaces/hello-af23464d222e/hel-728-a07b128af8f7/workdir/aigc-home/e2e/hel728.cjs:23:74 {
|
||||
log: [ " - waiting for locator('#desk-prompt') to be visible" ],
|
||||
name: 'TimeoutError'
|
||||
}
|
||||
FAILED SCRIPTS ['e2e/reference/check.cjs', 'e2e/reference/check-copy-fallback.cjs', 'e2e/hel728.cjs']
|
||||
@@ -0,0 +1,69 @@
|
||||
|
||||
> calliope-web@1.5.8 lint
|
||||
> prettier --check . && eslint .
|
||||
|
||||
Checking formatting...
|
||||
[warn] package-lock.json
|
||||
[warn] package.json
|
||||
[warn] scripts/check-storage-ssr.mjs
|
||||
[warn] src/hooks.client.ts
|
||||
[warn] src/hooks.server.ts
|
||||
[warn] src/lib/actions/spotlight.ts
|
||||
[warn] src/lib/agentComposer.ts
|
||||
[warn] src/lib/agentTasks.ts
|
||||
[warn] src/lib/assetPicker.ts
|
||||
[warn] src/lib/comfy/classifyInput.ts
|
||||
[warn] src/lib/comfy/parser.ts
|
||||
[warn] src/lib/comfy/promptInput.ts
|
||||
[warn] src/lib/comfy/resolution.ts
|
||||
[warn] src/lib/comfy/types.ts
|
||||
[warn] src/lib/comfy/useUpload.svelte.ts
|
||||
[warn] src/lib/components/agent/agentPalette.ts
|
||||
[warn] src/lib/components/generate/context.ts
|
||||
[warn] src/lib/components/generate/schema.ts
|
||||
[warn] src/lib/components/generate/takeDelete.svelte.ts
|
||||
[warn] src/lib/components/generate/takes.ts
|
||||
[warn] src/lib/components/ui/icons.ts
|
||||
[warn] src/lib/components/ui/modalLayers.ts
|
||||
[warn] src/lib/components/ui/README.md
|
||||
[warn] src/lib/dbtime.ts
|
||||
[warn] src/lib/durationBudget.ts
|
||||
[warn] src/lib/endpointKind.ts
|
||||
[warn] src/lib/events.ts
|
||||
[warn] src/lib/formOptions.ts
|
||||
[warn] src/lib/i18n.svelte.ts
|
||||
[warn] src/lib/i18n/de.ts
|
||||
[warn] src/lib/i18n/en.ts
|
||||
[warn] src/lib/i18n/es.ts
|
||||
[warn] src/lib/i18n/fr.ts
|
||||
[warn] src/lib/i18n/ja.ts
|
||||
[warn] src/lib/i18n/ko.ts
|
||||
[warn] src/lib/i18n/zh.ts
|
||||
[warn] src/lib/jobProgress.ts
|
||||
[warn] src/lib/jobStatus.ts
|
||||
[warn] src/lib/missingOutputs.svelte.ts
|
||||
[warn] src/lib/promptTemplates.ts
|
||||
[warn] src/lib/reduceMotion.svelte.ts
|
||||
[warn] src/lib/referenceAgent.ts
|
||||
[warn] src/lib/shot/api.ts
|
||||
[warn] src/lib/shot/calibration/compositionPresets.ts
|
||||
[warn] src/lib/shot/calibration/shotSolver.ts
|
||||
[warn] src/lib/shot/helpers/cameraUtils.ts
|
||||
[warn] src/lib/shot/helpers/jointConfig.ts
|
||||
[warn] src/lib/shot/helpers/mannequinFactory.ts
|
||||
[warn] src/lib/shot/helpers/mirror.ts
|
||||
[warn] src/lib/shot/helpers/posture.ts
|
||||
[warn] src/lib/shot/helpers/primitiveFactory.ts
|
||||
[warn] src/lib/shot/motion/sampleTrack.ts
|
||||
[warn] src/lib/shot/shotStore.svelte.ts
|
||||
[warn] src/lib/shot/types/mannequin-js.d.ts
|
||||
[warn] src/lib/storage.ts
|
||||
[warn] src/lib/toast.ts
|
||||
[warn] src/lib/wideLayout.ts
|
||||
[warn] src/routes/+layout.ts
|
||||
[warn] src/routes/+page.ts
|
||||
[warn] svelte.config.js
|
||||
[warn] tailwind.config.js
|
||||
[warn] tsconfig.json
|
||||
[warn] vite.config.ts
|
||||
[warn] Code style issues found in 63 files. Run Prettier with --write to fix.
|
||||
@@ -0,0 +1,442 @@
|
||||
.................................F...................................... [ 8%]
|
||||
........................................................................ [ 17%]
|
||||
........................................................................ [ 25%]
|
||||
........................................................................ [ 34%]
|
||||
..................................F..................................... [ 42%]
|
||||
........F............................................................... [ 51%]
|
||||
........................................................................ [ 59%]
|
||||
........................................................................ [ 68%]
|
||||
...F..........F......................................................... [ 76%]
|
||||
.........s.............................................................. [ 85%]
|
||||
........................................................................ [ 93%]
|
||||
sssss................................................ [100%]
|
||||
=================================== FAILURES ===================================
|
||||
________________________ test_comfy_server_info_scoping ________________________
|
||||
|
||||
def test_comfy_server_info_scoping():
|
||||
"""Native health check stays visible in both blind and linked sessions."""
|
||||
from calliope.agent.harness import tools as harness_tools
|
||||
|
||||
blind = ToolContext(session_id=1, project_id=None)
|
||||
linked = ToolContext(session_id=1, project_id=99)
|
||||
|
||||
> blind_names = {e["function"]["name"] for e in harness_tools.openai_tools_payload(blind)}
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
tests/test_agent_comfy_mcp.py:370:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
src/calliope/agent/harness/tools.py:27: in openai_tools_payload
|
||||
return _registry.openai_payload(ctx)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
src/calliope/agent/harness/registry.py:219: in openai_payload
|
||||
if self._visible(t, ctx)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
src/calliope/agent/harness/registry.py:203: in _visible
|
||||
if not user_allows_render(ctx):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
src/calliope/agent/harness/policy.py:156: in user_allows_render
|
||||
if has_structured_approval(ctx, "render"):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
src/calliope/agent/harness/policy.py:277: in has_structured_approval
|
||||
answer = latest_answer(ctx.session_id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
src/calliope/agent/harness/policy.py:260: in latest_answer
|
||||
events = read_events(session_id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
session_id = 1
|
||||
|
||||
def read_events(session_id: int) -> list[SessionEvent]:
|
||||
conn = _db()
|
||||
try:
|
||||
> rows = conn.execute(
|
||||
"SELECT * FROM agent_events WHERE session_id = ? ORDER BY seq", (session_id,)
|
||||
).fetchall()
|
||||
E sqlite3.OperationalError: no such table: agent_events
|
||||
|
||||
src/calliope/agent/harness/log.py:146: OperationalError
|
||||
___________ test_h3_messages_bind_neighbor_shots_and_keep_file_slots ___________
|
||||
|
||||
def test_h3_messages_bind_neighbor_shots_and_keep_file_slots():
|
||||
from calliope.agent.prompts import build_minimax_h3_ref_messages
|
||||
from calliope.agent.video_agent import resolve_h3_references
|
||||
from calliope.comfyui.parser import parse_dynamic_inputs
|
||||
|
||||
plan = {
|
||||
"overview": {"style": "noir", "lighting": "sodium"},
|
||||
"requirements": {"lighting": "sodium lamps only"},
|
||||
"shots": [
|
||||
{
|
||||
"clip_id": 1,
|
||||
"window": "00:00.000-00:04.000",
|
||||
"action": "enter",
|
||||
"camera": "push in",
|
||||
"lighting": "dim sodium",
|
||||
"speakers": "MAYA: hello",
|
||||
"spatial": "screen left",
|
||||
"state_after": "door open",
|
||||
},
|
||||
{
|
||||
"clip_id": 2,
|
||||
"window": "00:04.000-00:08.000",
|
||||
"action": "turn",
|
||||
"camera": "hold",
|
||||
"lighting": "dim sodium",
|
||||
"speakers": "",
|
||||
"spatial": "screen left",
|
||||
"state_after": "",
|
||||
},
|
||||
{
|
||||
"clip_id": 3,
|
||||
"window": "00:08.000-00:12.000",
|
||||
"action": "leave",
|
||||
"camera": "pull out",
|
||||
"lighting": "brighter",
|
||||
"speakers": "",
|
||||
"spatial": "",
|
||||
"state_after": "hallway empty",
|
||||
},
|
||||
],
|
||||
}
|
||||
lock = continuity_lock_text(plan, 2)
|
||||
assert "Previous shot" in lock
|
||||
assert "Next shot" in lock
|
||||
assert "Do not recast a subject" in lock
|
||||
assert "screen direction" in lock
|
||||
assert "enter" in lock and "leave" in lock
|
||||
|
||||
scene = {"heading": "INT. HALL", "action": "She turns.", "duration_sec": 4, "dialog": ""}
|
||||
subjects = [
|
||||
{
|
||||
"index": 1,
|
||||
"kind": "reference",
|
||||
"name": "mercs",
|
||||
"appearance": "",
|
||||
"path": r"E:\assets\mercs.png",
|
||||
}
|
||||
]
|
||||
messages = build_minimax_h3_ref_messages(scene, subjects, continuity=lock)
|
||||
text = messages[1]["content"]
|
||||
assert "Previous shot" in text
|
||||
assert "Next shot" in text
|
||||
assert "screen direction" in text
|
||||
assert "do not substitute another character or location" in text
|
||||
|
||||
workflow = {
|
||||
"148": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": ""},
|
||||
"_meta": {"title": "Ref 1 (Input:image)"},
|
||||
},
|
||||
"149": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": ""},
|
||||
"_meta": {"title": "Ref 2 (Input:image)"},
|
||||
},
|
||||
}
|
||||
inputs = parse_dynamic_inputs(workflow)
|
||||
resolved, paths, _videos = resolve_h3_references(
|
||||
inputs,
|
||||
{"148": r"E:\assets\hero.png", "149": r"E:\assets\mercs.png"},
|
||||
[{"name": "Maya", "appearance": "olive jacket", "sheet_path": r"E:\assets\maya.png"}],
|
||||
{"name": "Metro Line", "description": "rain-slick rooftops"},
|
||||
r"E:\assets\metro.png",
|
||||
)
|
||||
> assert resolved[1]["name"] == "mercs"
|
||||
E AssertionError: assert 'E:\\assets\\mercs' == 'mercs'
|
||||
E
|
||||
E - mercs
|
||||
E + E:\assets\mercs
|
||||
|
||||
tests/test_continuity.py:249: AssertionError
|
||||
___________ test_resolve_h3_references_prefers_form_files_over_story ___________
|
||||
|
||||
def test_resolve_h3_references_prefers_form_files_over_story():
|
||||
"""Slot files the user picked replace the story cast; empty slots keep it."""
|
||||
from calliope.agent.video_agent import resolve_h3_references
|
||||
from calliope.comfyui.parser import parse_dynamic_inputs
|
||||
|
||||
workflow = {
|
||||
"148": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": ""},
|
||||
"_meta": {"title": "Ref 1 (Input:image)"},
|
||||
},
|
||||
"149": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": ""},
|
||||
"_meta": {"title": "Ref 2 (Input:image)"},
|
||||
},
|
||||
"150": {
|
||||
"class_type": "LoadVideo",
|
||||
"inputs": {"video": ""},
|
||||
"_meta": {"title": "Motion (Input:video)"},
|
||||
},
|
||||
}
|
||||
inputs = parse_dynamic_inputs(workflow)
|
||||
characters = [
|
||||
{
|
||||
"name": "Maya",
|
||||
"appearance": "olive jacket",
|
||||
"sheet_path": r"E:\assets\maya.png",
|
||||
}
|
||||
]
|
||||
location = {"name": "Metro Line", "description": "rain-slick rooftops"}
|
||||
loc_image = r"E:\assets\metro.png"
|
||||
|
||||
empty_subjects, empty_paths, empty_videos = resolve_h3_references(
|
||||
inputs, {}, characters, location, loc_image
|
||||
)
|
||||
assert [s["name"] for s in empty_subjects] == ["Maya", "Metro Line"]
|
||||
assert empty_paths == [r"E:\assets\maya.png", r"E:\assets\metro.png"]
|
||||
assert empty_videos == []
|
||||
|
||||
subjects, paths, videos = resolve_h3_references(
|
||||
inputs,
|
||||
{
|
||||
"148": r"E:\assets\maya.png",
|
||||
"149": r"E:\assets\mercs.png",
|
||||
"150": r"E:\assets\fight.mp4",
|
||||
},
|
||||
characters,
|
||||
location,
|
||||
loc_image,
|
||||
)
|
||||
assert subjects[0]["kind"] == "character"
|
||||
assert subjects[0]["name"] == "Maya"
|
||||
assert subjects[1]["kind"] == "reference"
|
||||
> assert subjects[1]["name"] == "mercs"
|
||||
E AssertionError: assert 'E:\\assets\\mercs' == 'mercs'
|
||||
E
|
||||
E - mercs
|
||||
E + E:\assets\mercs
|
||||
|
||||
tests/test_h3_profile.py:215: AssertionError
|
||||
___________ test_preview_prompt_uses_form_references_not_story_cast ____________
|
||||
|
||||
client = <starlette.testclient.TestClient object at 0x7ff7be3182c0>
|
||||
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7ff7bc9b2180>
|
||||
|
||||
def test_preview_prompt_uses_form_references_not_story_cast(client, monkeypatch):
|
||||
"""The Video-tab image and video slots are the prompt's subjects.
|
||||
|
||||
Story character + location text must not replace files the user picked,
|
||||
and Regenerate (force) must not return a draft written before those files.
|
||||
"""
|
||||
pid = _mk_project(client, "Preview refs")
|
||||
scene = _add_scene(client, pid, 1, action="She swings the extinguisher.")
|
||||
clip_id = _scene_default_clip(pid, scene["id"])
|
||||
|
||||
wf = {
|
||||
"10": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": ""},
|
||||
"_meta": {"title": "Main Prompt (Input:prompt)"},
|
||||
},
|
||||
"148": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": ""},
|
||||
"_meta": {"title": "Ref 1 (Input:image)"},
|
||||
},
|
||||
"149": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": ""},
|
||||
"_meta": {"title": "Ref 2 (Input:image)"},
|
||||
},
|
||||
"150": {
|
||||
"class_type": "LoadVideo",
|
||||
"inputs": {"video": ""},
|
||||
"_meta": {"title": "Motion (Input:video)"},
|
||||
},
|
||||
}
|
||||
conn = get_db(settings.db_path)
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"""
|
||||
INSERT INTO workflows (name, kind, workflow_json, input_schema, output_schema,
|
||||
prompt_profile, is_enabled)
|
||||
VALUES (?, 'video', ?, '[]', '[]', 'minimax_h3_ref', 1)
|
||||
""",
|
||||
("H3 refs", json.dumps(wf)),
|
||||
)
|
||||
wf_id = cur.lastrowid
|
||||
char = conn.execute(
|
||||
"""
|
||||
INSERT INTO characters (project_id, name, appearance, sheet_path)
|
||||
VALUES (?, 'Maya', 'olive jacket', ?)
|
||||
""",
|
||||
(pid, r"E:\story\maya-sheet.png"),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO scene_characters (scene_id, character_id) VALUES (?, ?)",
|
||||
(scene["id"], char.lastrowid),
|
||||
)
|
||||
loc = conn.execute(
|
||||
"""
|
||||
INSERT INTO locations (project_id, name, description, reference_image_path)
|
||||
VALUES (?, 'Metro Line', 'rain-slick rooftops', ?)
|
||||
""",
|
||||
(pid, r"E:\story\metro.png"),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE scenes SET location_id = ? WHERE id = ?",
|
||||
(loc.lastrowid, scene["id"]),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE clips SET workflow_id = ? WHERE id = ?", (wf_id, clip_id)
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
seen: dict = {}
|
||||
|
||||
async def fake_rewrite(scene_, subjects, **kwargs):
|
||||
seen["subjects"] = subjects
|
||||
seen["videos"] = kwargs.get("videos")
|
||||
seen["calls"] = seen.get("calls", 0) + 1
|
||||
return "REWRITE FROM REFS"
|
||||
|
||||
monkeypatch.setattr("calliope.agent.video_agent._h3_rewrite", fake_rewrite)
|
||||
|
||||
form = {
|
||||
"148": r"E:\refs\hero.png",
|
||||
"149": r"E:\refs\mercs.png",
|
||||
"150": r"E:\refs\fight.mp4",
|
||||
}
|
||||
r = client.post(
|
||||
f"/api/jobs/projects/{pid}/preview-prompt",
|
||||
json={"clip_id": clip_id, "input_values": form},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["prompt"] == "REWRITE FROM REFS"
|
||||
assert body["from_draft"] is False
|
||||
names = [s["name"] for s in seen["subjects"]]
|
||||
> assert names == ["hero", "mercs"]
|
||||
E AssertionError: assert ['E:\\refs\\h...\refs\\mercs'] == ['hero', 'mercs']
|
||||
E
|
||||
E At index 0 diff: 'E:\\refs\\hero' != 'hero'
|
||||
E Use -v to get more diff
|
||||
|
||||
tests/test_scene_settings.py:487: AssertionError
|
||||
---------------------------- Captured stderr setup -----------------------------
|
||||
2026-10-21 19:58:18,478 | INFO | calliope | Calliope started — db=/tmp/multica-task-3084682389/tmp_95dfjk4/calliope.db dry_run=True
|
||||
2026-10-21 19:58:18,478 | INFO | calliope.worker | Queue worker started
|
||||
------------------------------ Captured log setup ------------------------------
|
||||
INFO calliope:main.py:112 Calliope started — db=/tmp/multica-task-3084682389/tmp_95dfjk4/calliope.db dry_run=True
|
||||
INFO calliope.worker:worker.py:79 Queue worker started
|
||||
----------------------------- Captured stderr call -----------------------------
|
||||
2026-10-21 19:58:18,482 | INFO | httpx2 | HTTP Request: POST http://testserver/api/projects "HTTP/1.1 200 OK"
|
||||
2026-10-21 19:58:18,495 | INFO | httpx2 | HTTP Request: POST http://testserver/api/projects/1/scenes "HTTP/1.1 200 OK"
|
||||
2026-10-21 19:58:18,506 | INFO | httpx2 | HTTP Request: POST http://testserver/api/jobs/projects/1/preview-prompt "HTTP/1.1 200 OK"
|
||||
------------------------------ Captured log call -------------------------------
|
||||
INFO httpx2:_client.py:1085 HTTP Request: POST http://testserver/api/projects "HTTP/1.1 200 OK"
|
||||
INFO httpx2:_client.py:1085 HTTP Request: POST http://testserver/api/projects/1/scenes "HTTP/1.1 200 OK"
|
||||
INFO httpx2:_client.py:1085 HTTP Request: POST http://testserver/api/jobs/projects/1/preview-prompt "HTTP/1.1 200 OK"
|
||||
--------------------------- Captured stderr teardown ---------------------------
|
||||
2026-10-21 19:58:18,980 | INFO | calliope.worker | Queue worker stopped
|
||||
2026-10-21 19:58:18,980 | INFO | calliope | Calliope shutting down
|
||||
---------------------------- Captured log teardown -----------------------------
|
||||
INFO calliope.worker:worker.py:185 Queue worker stopped
|
||||
INFO calliope:main.py:118 Calliope shutting down
|
||||
_________________ test_script_regenerate_keeps_expanded_count __________________
|
||||
|
||||
client = <starlette.testclient.TestClient object at 0x7ff7bd902660>
|
||||
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7ff7bc9b2180>
|
||||
|
||||
def test_script_regenerate_keeps_expanded_count(client, monkeypatch):
|
||||
calls: list[int] = []
|
||||
|
||||
async def fake_structured(messages, temperature=0.7):
|
||||
user = messages[1]["content"]
|
||||
# Parse required count from prompt
|
||||
n = 4
|
||||
for line in user.splitlines():
|
||||
if line.startswith("required_scene_count:"):
|
||||
n = int(line.split(":", 1)[1].strip())
|
||||
break
|
||||
calls.append(n)
|
||||
return {
|
||||
"scenes": [
|
||||
{
|
||||
"order_index": i,
|
||||
"heading": f"SCENE {i}",
|
||||
"action": f"action {i}",
|
||||
"dialog": "",
|
||||
"duration_sec": 5,
|
||||
"character_ids": [],
|
||||
"location_id": None,
|
||||
}
|
||||
for i in range(1, n + 1)
|
||||
]
|
||||
}
|
||||
|
||||
monkeypatch.setattr("calliope.agent.script_agent.generate_structured", fake_structured)
|
||||
|
||||
r = client.post("/api/projects", json={"title": "Expand", "idea": "desert", "target_duration": "30 seconds"})
|
||||
pid = r.json()["id"]
|
||||
|
||||
# Seed 4 scenes via generate
|
||||
r = client.post(f"/api/projects/{pid}/generate-script", json={"replace": True})
|
||||
> assert r.status_code == 200
|
||||
E assert 500 == 200
|
||||
E + where 500 = <Response [500 Internal Server Error]>.status_code
|
||||
|
||||
tests/test_script_scene_count.py:59: AssertionError
|
||||
---------------------------- Captured stderr setup -----------------------------
|
||||
2026-10-21 19:58:19,584 | INFO | calliope | Calliope started — db=/tmp/multica-task-3084682389/tmpl0t0r_mx/calliope.db dry_run=True
|
||||
2026-10-21 19:58:19,584 | INFO | calliope.worker | Queue worker started
|
||||
------------------------------ Captured log setup ------------------------------
|
||||
INFO calliope:main.py:112 Calliope started — db=/tmp/multica-task-3084682389/tmpl0t0r_mx/calliope.db dry_run=True
|
||||
INFO calliope.worker:worker.py:79 Queue worker started
|
||||
----------------------------- Captured stderr call -----------------------------
|
||||
2026-10-21 19:58:19,588 | INFO | httpx2 | HTTP Request: POST http://testserver/api/projects "HTTP/1.1 200 OK"
|
||||
2026-10-21 19:58:19,604 | INFO | calliope.llm | LLM stream request to http://127.0.0.1:11434/v1/chat/completions with model llama3.2
|
||||
2026-10-21 19:58:19,606 | INFO | httpx2 | HTTP Request: POST http://testserver/api/projects/1/generate-script "HTTP/1.1 500 Internal Server Error"
|
||||
------------------------------ Captured log call -------------------------------
|
||||
INFO httpx2:_client.py:1085 HTTP Request: POST http://testserver/api/projects "HTTP/1.1 200 OK"
|
||||
INFO calliope.llm:llm.py:360 LLM stream request to http://127.0.0.1:11434/v1/chat/completions with model llama3.2
|
||||
INFO httpx2:_client.py:1085 HTTP Request: POST http://testserver/api/projects/1/generate-script "HTTP/1.1 500 Internal Server Error"
|
||||
--------------------------- Captured stderr teardown ---------------------------
|
||||
2026-10-21 19:58:20,086 | INFO | calliope.worker | Queue worker stopped
|
||||
2026-10-21 19:58:20,086 | INFO | calliope | Calliope shutting down
|
||||
---------------------------- Captured log teardown -----------------------------
|
||||
INFO calliope.worker:worker.py:185 Queue worker stopped
|
||||
INFO calliope:main.py:118 Calliope shutting down
|
||||
=============================== warnings summary ===============================
|
||||
.venv/lib/python3.12/site-packages/starlette/testclient.py:53
|
||||
/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-728-a07b128af8f7/workdir/aigc-home/calliope-backend/.venv/lib/python3.12/site-packages/starlette/testclient.py:53: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
|
||||
_PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]
|
||||
|
||||
tests/test_shell_tool.py::test_settings_roundtrip_for_new_keys
|
||||
/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-728-a07b128af8f7/workdir/aigc-home/calliope-backend/.venv/lib/python3.12/site-packages/_pytest/unraisableexception.py:67: PytestUnraisableExceptionWarning: Exception ignored in: <function BaseSubprocessTransport.__del__ at 0x7ff7c055cb80>
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/home/xiaomo/.local/share/uv/python/cpython-3.12.14-linux-x86_64-gnu/lib/python3.12/asyncio/base_subprocess.py", line 126, in __del__
|
||||
self.close()
|
||||
File "/home/xiaomo/.local/share/uv/python/cpython-3.12.14-linux-x86_64-gnu/lib/python3.12/asyncio/base_subprocess.py", line 104, in close
|
||||
proto.pipe.close()
|
||||
File "/home/xiaomo/.local/share/uv/python/cpython-3.12.14-linux-x86_64-gnu/lib/python3.12/asyncio/unix_events.py", line 568, in close
|
||||
self._close(None)
|
||||
File "/home/xiaomo/.local/share/uv/python/cpython-3.12.14-linux-x86_64-gnu/lib/python3.12/asyncio/unix_events.py", line 592, in _close
|
||||
self._loop.call_soon(self._call_connection_lost, exc)
|
||||
File "/home/xiaomo/.local/share/uv/python/cpython-3.12.14-linux-x86_64-gnu/lib/python3.12/asyncio/base_events.py", line 799, in call_soon
|
||||
self._check_closed()
|
||||
File "/home/xiaomo/.local/share/uv/python/cpython-3.12.14-linux-x86_64-gnu/lib/python3.12/asyncio/base_events.py", line 545, in _check_closed
|
||||
raise RuntimeError('Event loop is closed')
|
||||
RuntimeError: Event loop is closed
|
||||
|
||||
Enable tracemalloc to get traceback where the object was allocated.
|
||||
See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.
|
||||
warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
=========================== short test summary info ============================
|
||||
FAILED tests/test_agent_comfy_mcp.py::test_comfy_server_info_scoping - sqlite...
|
||||
FAILED tests/test_continuity.py::test_h3_messages_bind_neighbor_shots_and_keep_file_slots
|
||||
FAILED tests/test_h3_profile.py::test_resolve_h3_references_prefers_form_files_over_story
|
||||
FAILED tests/test_scene_settings.py::test_preview_prompt_uses_form_references_not_story_cast
|
||||
FAILED tests/test_script_scene_count.py::test_script_regenerate_keeps_expanded_count
|
||||
5 failed, 834 passed, 6 skipped, 2 warnings in 368.55s (0:06:08)
|
||||
@@ -0,0 +1,201 @@
|
||||
feat/hel-665-refract @ 35fd07a (v5 formal interfaces; stage advice API; user database/config unchanged; demos retained; rollback /opt/aigc-home/releases/rollback-HEL665-v5-35fd07a-20261015.sh)
|
||||
total 64
|
||||
drwxr-xr-x 4 root root 4096 Oct 15 00:33 .
|
||||
drwxr-xr-x 10 root root 12288 Oct 15 00:33 ..
|
||||
drwxrwxr-x 6 mobai mobai 4096 Oct 15 00:33 build
|
||||
drwxrwxr-x 4 mobai mobai 4096 Oct 10 20:35 src
|
||||
-rw-rw-r-- 1 mobai mobai 40144 Oct 15 00:13 workbench.py
|
||||
6864465552ed31ae9179bf6cb8b45bc7df4e43b1b5d452abaa04007346cfa50c /opt/aigc-home/app/calliope-web/src/app.html
|
||||
9fcd4c28d99551f12eebc3e58d2d2e715a2d2fae91315078e25ad301a3fe0101 /opt/aigc-home/app/calliope-web/src/hooks.client.ts
|
||||
016119d7d2f2f77dee4441ce8c0e0c337461775be19589ab25eb8e6d93d9325b /opt/aigc-home/app/calliope-web/src/hooks.server.ts
|
||||
90ef4a1441f5ea1b9b1191dcd90bdedfad038a834393860071f4ac658217234a /opt/aigc-home/app/calliope-web/src/lib/actions/spotlight.ts
|
||||
b73c6e2c597d2ee06071c279ea7f51b8318702ec09617a29361d906fd72e3f12 /opt/aigc-home/app/calliope-web/src/lib/agentComposer.ts
|
||||
f5b7e10ecef0822206220c8c2827740c3e35254ca92ce130d52a786945dc3af8 /opt/aigc-home/app/calliope-web/src/lib/agentTasks.ts
|
||||
859129cc60fab17c6104f82c82eb0dc42322d9f81fb8b210ba4ff5aace234e4a /opt/aigc-home/app/calliope-web/src/lib/api.ts
|
||||
53fa1c405c185b2ec07cd379011dde429de28c965a8d7e21771e1a5b21e3e79c /opt/aigc-home/app/calliope-web/src/lib/assetPicker.ts
|
||||
b0adfec622f00ee5dbe1f786c9d487229ddb33eb2077e2166bd7b391c0d32d6c /opt/aigc-home/app/calliope-web/src/lib/canvas/ArtifactNode.svelte
|
||||
4d76e5fd32270d79be2814c2bd6b9ba19f1c3e5319133d915966052ecc92f6f3 /opt/aigc-home/app/calliope-web/src/lib/canvas/EntityNode.svelte
|
||||
8bc6d779cdfc98885b5203dabdca38999e12e9d503dfd6d7cd8452162b0bd78e /opt/aigc-home/app/calliope-web/src/lib/comfy/classifyInput.ts
|
||||
b0a8e133e68635ba08f700c23196a7ba9524d510ac5d28d8aa674cf18c21bd70 /opt/aigc-home/app/calliope-web/src/lib/comfy/imageFile.ts
|
||||
e8372ec223a04e4aef7af92cbe11656acc1339bfbc6029876efb4495efac60b1 /opt/aigc-home/app/calliope-web/src/lib/comfy/parser.ts
|
||||
233160c907bcf7a6b3a6d78d5500cecdf2e0a14dbd67b2f7a63555f721106c91 /opt/aigc-home/app/calliope-web/src/lib/comfy/promptInput.ts
|
||||
4cbfcb95e28e2d5061614977831b2a9fe1c82449ab9ed5b692d963e1d99f6505 /opt/aigc-home/app/calliope-web/src/lib/comfy/resolution.ts
|
||||
0edcc73d9a03a38804032b8799705f85d5a8560838ddbafd33651ea167a354c6 /opt/aigc-home/app/calliope-web/src/lib/comfy/types.ts
|
||||
aa91e6d36c34dd9a5246fff3c24e4b437dccb8f9c55141959a9b1df0639205b9 /opt/aigc-home/app/calliope-web/src/lib/comfy/useUpload.svelte.ts
|
||||
3722615c86e150d61fee60b287cc66fc1ff5f32c4a13e55f7f2e4ec6e3b7e95c /opt/aigc-home/app/calliope-web/src/lib/components/AppHeader.svelte
|
||||
b6844910420c977167b7beee5afa6ee3f649a74bd6e8128ced432e3a51079e57 /opt/aigc-home/app/calliope-web/src/lib/components/AttachToProject.svelte
|
||||
70b01844b1b8bb3c1c79e49aa61d3a37fe93ccf30c1f28a1e05867efa8f58582 /opt/aigc-home/app/calliope-web/src/lib/components/ComfyDynamicForm.svelte
|
||||
b67433cd628d0507665fdd594bbf38ba366cb71cf6e1bcf32c8be05cbc8bddc5 /opt/aigc-home/app/calliope-web/src/lib/components/GeneratingOverlay.svelte
|
||||
6675bd98205aa274938874dba40e4c95f0f871b9ea29433cb5edf207a8963a3d /opt/aigc-home/app/calliope-web/src/lib/components/ImageLightbox.svelte
|
||||
37fb771f258dd4dfe7b4c1738b84ea6aa74cd35508505c0c03bbb2121226c4ac /opt/aigc-home/app/calliope-web/src/lib/components/LanguageSwitcher.svelte
|
||||
4552e1fd1972a55b7267949fe6f6ce57e18b0c500698e2f919562a77b3ffa636 /opt/aigc-home/app/calliope-web/src/lib/components/OmniComposer.svelte
|
||||
d53835230e1d584e59c98f5b6aa965b605ab73011f33eac61a0a0e44158b6d0b /opt/aigc-home/app/calliope-web/src/lib/components/SafeMedia.svelte
|
||||
4becfa6663461e91602b74322f9807aedd403522ccd54603c66b80b872363953 /opt/aigc-home/app/calliope-web/src/lib/components/ToastHost.svelte
|
||||
d744fc2278a627ddb329a0ba9c86b80905d09e58a476297a35c4c77874c35b07 /opt/aigc-home/app/calliope-web/src/lib/components/WorkflowImportDrawer.svelte
|
||||
a75f0602d09cf72d6821a69be537cfda284ff589a9f5452e7ed8dd64337b7bd4 /opt/aigc-home/app/calliope-web/src/lib/components/WorkflowTestDialog.svelte
|
||||
52d9b005c65f6d83ef7ead5caa81dc61058a7049c29779b76d01979361c480b7 /opt/aigc-home/app/calliope-web/src/lib/components/agent/AgentArtifactCard.svelte
|
||||
075ab299124430e41ca9ab1e314d3e616dd0fbdc9e36cf92cd7b0479fdd493f4 /opt/aigc-home/app/calliope-web/src/lib/components/agent/AgentChat.svelte
|
||||
74545d012fa615b92d69ce6c5b660d4f61ba2501fe593a3cb92fd8df40136df6 /opt/aigc-home/app/calliope-web/src/lib/components/agent/AgentComposer.svelte
|
||||
abc7741fd784affd82a7a38b2d10d1714b77170da866cf2ea89ced3d43bfaa46 /opt/aigc-home/app/calliope-web/src/lib/components/agent/AgentPlanPanel.svelte
|
||||
3b87fdf4e8f2077cf0407bb2eadc418ca9e8aa4e781640b58ff9d88df73fc67f /opt/aigc-home/app/calliope-web/src/lib/components/agent/AgentSessionSidebar.svelte
|
||||
c1af4f03f60b8c677253c804316bfbc9629eb78d9846b5cff7d574befa1216e6 /opt/aigc-home/app/calliope-web/src/lib/components/agent/AgentToolCard.svelte
|
||||
932dcf780eec9d3f294bf90dbb04d25098b4a8c36a31113a7bb10162251c1e4e /opt/aigc-home/app/calliope-web/src/lib/components/agent/ReasoningPanel.svelte
|
||||
6dfc7d0b3a2372e3dd5213c48a7e5c6a7c2bb109c7fe102b9ddfab8f42217e77 /opt/aigc-home/app/calliope-web/src/lib/components/agent/WorkflowMentionMenu.svelte
|
||||
a84cf30f6cf6356c09b2623201e1f099f2843408553c724f76e362f5d844d2f7 /opt/aigc-home/app/calliope-web/src/lib/components/agent/agentPalette.ts
|
||||
770c0feb2009286ebdc455228a67117a80f94f58e4728475c17c57d968910f4c /opt/aigc-home/app/calliope-web/src/lib/components/desk/Artboard.svelte
|
||||
fe8483c2f5a4fdf9df112f0feae1d3170162b5c0fbf5bae73c1b6cc3126f2a31 /opt/aigc-home/app/calliope-web/src/lib/components/desk/DeskWorkflowRail.svelte
|
||||
f445d83de06804f132680eab34a6412444f1885a67a1aa832e62307f649ddaf3 /opt/aigc-home/app/calliope-web/src/lib/components/desk/InputRequirementBadge.svelte
|
||||
da087b4e93feeee3def4bafdf0ac931630fcd5b87b2293c4102e9b090eb3734f /opt/aigc-home/app/calliope-web/src/lib/components/desk/LibraryFilters.svelte
|
||||
cbdad9bfe085513184e2c30aefabaa92dbef38de4cbf2af763e2f74ad29e75e3 /opt/aigc-home/app/calliope-web/src/lib/components/desk/LibraryRow.svelte
|
||||
28583dfa4bcd6e605d3ba41ac889fdbd29d592949153a93bdd3670246a0a33cc /opt/aigc-home/app/calliope-web/src/lib/components/desk/ResultRail.svelte
|
||||
d78d68e3e3858d8cf0f22d06c9bb616187241264e3c67710f9f7561bb5ec4133 /opt/aigc-home/app/calliope-web/src/lib/components/desk/StudioDesk.svelte
|
||||
77518af58ceaaf4026eda2df5191ba244fff4b3ef21db54ae8692b759be86a6a /opt/aigc-home/app/calliope-web/src/lib/components/desk/TaskModeRail.svelte
|
||||
7bd0699f0d83702e04a06adc4cff8cdc4f62f5e6581b5c2deeb0e0b05d2077d7 /opt/aigc-home/app/calliope-web/src/lib/components/desk/WorkflowBay.svelte
|
||||
c093d1664194022d3edfb782d735df60e93a6fe838afb5ccbfb9b6079b2b1023 /opt/aigc-home/app/calliope-web/src/lib/components/desk/WorkflowPreview.svelte
|
||||
281bf5a5795b17d468b12dd4973004e5bdac458cd55e061feaaed69aa2c218c5 /opt/aigc-home/app/calliope-web/src/lib/components/generate/Composer.svelte
|
||||
ef72dc2367471507a4b5d15621854c761b99bba571876b94b07852369646260e /opt/aigc-home/app/calliope-web/src/lib/components/generate/ContextBar.svelte
|
||||
0e2943e5120efcf9ad620ba1d8a4e4c1658c88560c7c1d9b4032cd960f0a097a /opt/aigc-home/app/calliope-web/src/lib/components/generate/Inspector.svelte
|
||||
32f4c7c1a3a571a240741255fdf83173ef5d9d0f0a4626e02ea19d88fe613980 /opt/aigc-home/app/calliope-web/src/lib/components/generate/ParamChip.svelte
|
||||
1c7726496d94f822b1f6a72ccd7e0fd1a40b91e3adf64edc45f995d71df5da2b /opt/aigc-home/app/calliope-web/src/lib/components/generate/ParameterPopover.svelte
|
||||
18e20e2685e840f018b2eb9ba1928af5bd4fdf4cd8c2b2d65fd1f01da644aca8 /opt/aigc-home/app/calliope-web/src/lib/components/generate/ParamsPanel.svelte
|
||||
dd3d58c3bf9901d25790f44cc2eaa5835ea15335c3695202f565bb640efb2ed7 /opt/aigc-home/app/calliope-web/src/lib/components/generate/SlotTray.svelte
|
||||
f8205126227507fe1b50c9f57fec6412b830ad2234bf1ee61a5a458bb82ca3a2 /opt/aigc-home/app/calliope-web/src/lib/components/generate/TakeCard.svelte
|
||||
2307ee5f013d81f20315ec2118d801a3367837c8d018048d501f65770a173f26 /opt/aigc-home/app/calliope-web/src/lib/components/generate/TakeFeed.svelte
|
||||
beb37354c781810c670661be8ac35ccdf6b8fc9d23525a4fcc543cdb10b6ad0a /opt/aigc-home/app/calliope-web/src/lib/components/generate/context.ts
|
||||
da980feb2ea7e0eec93fac5cc0ee0c01a6be6f80d3c685e7eb22ecd00cc8741b /opt/aigc-home/app/calliope-web/src/lib/components/generate/schema.ts
|
||||
5cfda8936144415a0e001511128e39268cf4db39430b59882ff4ae80da9c93c6 /opt/aigc-home/app/calliope-web/src/lib/components/generate/takeDelete.svelte.ts
|
||||
ea14c626f5bedec1b8fb607af318e05454b36f913a63ad4808d9026380e0687d /opt/aigc-home/app/calliope-web/src/lib/components/generate/takes.ts
|
||||
eed1c80c2bb5ed54737c94adcc8f6461e57fb182218fca6ae0f87c61b0a1a0f8 /opt/aigc-home/app/calliope-web/src/lib/components/home/AProjectCard.svelte
|
||||
82de633bef1c92fecedd0618faaf4f3c6e869f365f8d22eda1631921f8cf56a5 /opt/aigc-home/app/calliope-web/src/lib/components/omni/AssetPickerModal.svelte
|
||||
0ba296a2ea05bb78ab3fdcd1ea526d894eb3eed0ed74f9149025abd68774413a /opt/aigc-home/app/calliope-web/src/lib/components/omni/MediaTile.svelte
|
||||
e587b7b07272f5f3c9935dce6092cb5fb4e678d0fa3653a02360f4dec7f478ae /opt/aigc-home/app/calliope-web/src/lib/components/omni/PillPopover.svelte
|
||||
47f04edf92ad2336893f77744762effef6e4acfeb53d657524ce1005505fb240 /opt/aigc-home/app/calliope-web/src/lib/components/omni/PillSelect.svelte
|
||||
5ef968def3ce3d7e134ef5ac10d07b098a9ec7a336593494ff7d967f7ff3158e /opt/aigc-home/app/calliope-web/src/lib/components/omni/PillStepper.svelte
|
||||
b895e524829484dd7306f1bb4cc2e9ce652a1968662c4f4d6a7d586cea6ba4f8 /opt/aigc-home/app/calliope-web/src/lib/components/refract/Hero.svelte
|
||||
e7399561fa10f35aaaffe4d085b1822e626a0396057883d4b5b5c24462f26a15 /opt/aigc-home/app/calliope-web/src/lib/components/settings/MemoryPanel.svelte
|
||||
24b303e3e0f2db36dafebef71353611f34fb75ac851730a1551621aaf1d0e219 /opt/aigc-home/app/calliope-web/src/lib/components/shell/AppShell.svelte
|
||||
25e74bb4200011048065b5c7de7566da902e2ed456316fdf786de1b1652d767a /opt/aigc-home/app/calliope-web/src/lib/components/shell/CommandPalette.svelte
|
||||
87a214fb486b27a8a7ed38caaef39b736a2fddb8be9b5a09c051762caf263442 /opt/aigc-home/app/calliope-web/src/lib/components/shell/ProjectScope.svelte
|
||||
9ebcf08567795b431537942481b461e400ab6a37a60e927e3fe559ddce6e65eb /opt/aigc-home/app/calliope-web/src/lib/components/shell/QueueTray.svelte
|
||||
fd205da624befed52999700d8a00ec48f167c376c51258b791ec4d8a2a345ac9 /opt/aigc-home/app/calliope-web/src/lib/components/shell/ShellPop.svelte
|
||||
f07a172763a2b0a7755ea20721ae20d7b2cb63aaeb5a6f3229c4ddedf99199e9 /opt/aigc-home/app/calliope-web/src/lib/components/shell/nav.ts
|
||||
16a3e74a27530b0a0065e425c128de12168243c5b35897f47bb5b7c9be2ae0c3 /opt/aigc-home/app/calliope-web/src/lib/components/studio/MediaSlot.svelte
|
||||
8b0e79f5d2563d2a73c61dad5dc721680d441787b26081dd2887097a9d9d1424 /opt/aigc-home/app/calliope-web/src/lib/components/studio/RefImageGrid.svelte
|
||||
b55a01704dad5ecdab05d3bddeb1d6e44923322fa7a9d4aa60558a702a231e07 /opt/aigc-home/app/calliope-web/src/lib/components/ui/Button.svelte
|
||||
624cbcb67c9dc143a75b78359cd8c50c067dbff57cea528a7a309e0be29d8d96 /opt/aigc-home/app/calliope-web/src/lib/components/ui/Card.svelte
|
||||
a72c982126332293887705350c8825f6232e2e1067abaf4a1bbfe1773aac473c /opt/aigc-home/app/calliope-web/src/lib/components/ui/ConfirmDialog.svelte
|
||||
bd56ad82b41fa23f64e908984c7a4c3ae6f24bdd5d1cfe2e66e21de25446d5ca /opt/aigc-home/app/calliope-web/src/lib/components/ui/Drawer.svelte
|
||||
7ab297b1f73b8bde8b7389f52721e2216fecdd4df91e5fefb21e424336194f30 /opt/aigc-home/app/calliope-web/src/lib/components/ui/EmptyState.svelte
|
||||
81798dc7007582e1025c246d4c3e9bb9ccfe7306f3199475b1581e58382005a5 /opt/aigc-home/app/calliope-web/src/lib/components/ui/Icon.svelte
|
||||
db6130e0930b5a92d6af7fd613a898dcd117a9a7268823c1544d9c150cefe92d /opt/aigc-home/app/calliope-web/src/lib/components/ui/Modal.svelte
|
||||
0925761862b026b218cea0d6464cff4e67f1181bf4df4ff06f91e8ae472ea292 /opt/aigc-home/app/calliope-web/src/lib/components/ui/ProgressBar.svelte
|
||||
8f9304f189cb51702be7d426aad14edc05d6c68707baf8448cfe6de6a7951fed /opt/aigc-home/app/calliope-web/src/lib/components/ui/README.md
|
||||
08e0d4f85cc945a1fa3773edbbb8c725b7f9737bf5be5e7cd2a679bd9f9f90c1 /opt/aigc-home/app/calliope-web/src/lib/components/ui/Skeleton.svelte
|
||||
1460161d521f5ffaf1853ccb85e33240bce1ce736fd4a2e0e081acdd7cd2c584 /opt/aigc-home/app/calliope-web/src/lib/components/ui/Spinner.svelte
|
||||
c67bc6d80dc82801f9e9be21c201781dd2d0708dad857ee3425f42066ce5c6be /opt/aigc-home/app/calliope-web/src/lib/components/ui/StatusChip.svelte
|
||||
bbe0468da5495496bb76b75624eecd18438346b9c111319a77e3a2936f0a15f8 /opt/aigc-home/app/calliope-web/src/lib/components/ui/Switch.svelte
|
||||
c1c0c2f9edca20d964eddad0d02e53a1b332c53cef2dc8d5636cdf7af5bdef33 /opt/aigc-home/app/calliope-web/src/lib/components/ui/icons.ts
|
||||
0ab42d1c4781f6aa63aaabe90d40062d5f633a9941b10ed9c2dc7f5fc5a2472d /opt/aigc-home/app/calliope-web/src/lib/components/ui/modalLayers.ts
|
||||
61c450b53fc546a2f1b68551b691326abefd705fc6145287f25ea48b3c4b269f /opt/aigc-home/app/calliope-web/src/lib/components/video/ClipMonitor.svelte
|
||||
62649ec0b2e02c33b60651b31927bca0a589e3f68bc665d50438aa71bfe16161 /opt/aigc-home/app/calliope-web/src/lib/components/video/ClipSourceModal.svelte
|
||||
23225ace57828aa80cbd37fc00156b4170d31ddfbebb693c747059ad2032a280 /opt/aigc-home/app/calliope-web/src/lib/components/video/JobInputsDrawer.svelte
|
||||
38df3060885270e2e0d4e7a0eecad7d460fd88b5d65f3f65e6d3ba87ea9e3744 /opt/aigc-home/app/calliope-web/src/lib/components/video/PromptPreviewModal.svelte
|
||||
cde0d361ab0b5eb220da310d6297092c9c0cc8fb3b44a53eab84a1acc898ac1b /opt/aigc-home/app/calliope-web/src/lib/components/video/SceneFilmstrip.svelte
|
||||
116be76c6f77cd763279bbc632fefe9d5e1b8a220533ee92776e49a34e73e6cd /opt/aigc-home/app/calliope-web/src/lib/components/video/SceneScriptDrawer.svelte
|
||||
bcaa50dd928ae2b1ecf95697fad51852c2bf091ff391e94e749eb09035521322 /opt/aigc-home/app/calliope-web/src/lib/components/video/ShotBrief.svelte
|
||||
fe5823233fd94847258f02a8dbba7df1d1c84f0f45a9fde459dad297221f7e52 /opt/aigc-home/app/calliope-web/src/lib/components/video/VideoEditWorkspace.svelte
|
||||
f0d8365d82b65dcbb995cf93fe4769705bf9d9136b1bd60eb0e26387ccd488ec /opt/aigc-home/app/calliope-web/src/lib/components/workbench/AssetsStagePanel.svelte
|
||||
91c7eb1b74135fdb4780e86871d646dcaa23c6c7cd1d8d0cc69bc4ec044bfd99 /opt/aigc-home/app/calliope-web/src/lib/components/workbench/JobChip.svelte
|
||||
a9b75b36f0c2f061caf61a3fe897ad4799a2cbeb3fbdf4cd90c907321a9e3869 /opt/aigc-home/app/calliope-web/src/lib/components/workbench/PromptBlock.svelte
|
||||
6a01205bf0e4118f38a988f359650a0e9c3522567e7328e6783a32abb7a4ce03 /opt/aigc-home/app/calliope-web/src/lib/components/workbench/ScriptStagePanel.svelte
|
||||
0cc25c48ed8bcc5d1312cae5da8ec8d43a719b88de1e9f1aecc34d216884ffbd /opt/aigc-home/app/calliope-web/src/lib/components/workbench/ShotStagePanel.svelte
|
||||
b64c479ec6e262c10607fd7bb9c5e3096e7ec9870369754e288f95293abb6fb2 /opt/aigc-home/app/calliope-web/src/lib/components/workbench/StageStepper.svelte
|
||||
afd4ffe76429630d4ee6e30c06cc9e121a637e252bff78b0ff902990b1827c66 /opt/aigc-home/app/calliope-web/src/lib/components/workbench/StoryboardStagePanel.svelte
|
||||
1b23da03f59c5d186e7e760c606e11e7fe22cb37ae8d90cfb813efc0dad9d597 /opt/aigc-home/app/calliope-web/src/lib/components/workbench/StyleStagePanel.svelte
|
||||
58407122bbb4c9cf4a4d3c280b7e27af7a8a51141ce2dd3eabe91644ea7843f7 /opt/aigc-home/app/calliope-web/src/lib/components/workflows/WorkflowBatchImportWizard.svelte
|
||||
77a2ac092a4c0bbb1f952e191b88704edd5815b783f439a022cd4f4baca5d16a /opt/aigc-home/app/calliope-web/src/lib/components/workflows/WorkflowLibrary.svelte
|
||||
d193924f8d50ac86de09cb8b1d44511c637811dbe5fb12bf8826633f5cac6d88 /opt/aigc-home/app/calliope-web/src/lib/components/workflows/WorkflowMetaDialog.svelte
|
||||
f7e318b7c2be62b8075568eaa1cfebbb002a5ac554cf26b7c6367ee6429de224 /opt/aigc-home/app/calliope-web/src/lib/dbtime.ts
|
||||
cdbcb72007dbc24c5fc13377aeeae8851644b306eb376e872155fda179c7acd3 /opt/aigc-home/app/calliope-web/src/lib/durationBudget.ts
|
||||
161c8c35241d55c6643f946e092d65748b8d55c6fdf5dad0fadf6e37e4fb3208 /opt/aigc-home/app/calliope-web/src/lib/endpointKind.ts
|
||||
d8cfc840dcccffadf3546d07ed1fe32c43e86b85599149e756ea80871ca3706a /opt/aigc-home/app/calliope-web/src/lib/events.ts
|
||||
9ae9634200ef0aaec1050a3638b32e2e1cf1dfdae089dca067a35cd2155fa7ec /opt/aigc-home/app/calliope-web/src/lib/formOptions.ts
|
||||
e22719d77911c34c6d49ce6fbffb655fbb9053bbe0a471cfd7bb36804e1adb89 /opt/aigc-home/app/calliope-web/src/lib/i18n.svelte.ts
|
||||
b48252457d2ce992db05b82dad361f3f971504a5b7c4e1f78f7e4f59eb062afc /opt/aigc-home/app/calliope-web/src/lib/i18n/de.ts
|
||||
8cc8df905076d916cbde2ba83ad3202ab3cfb87e38d4d2119023404dfaa6b7fc /opt/aigc-home/app/calliope-web/src/lib/i18n/en.ts
|
||||
837bf76c0ae34be1f6348bea071f47227ca0573d4c466574ad2ed99eb6db7df0 /opt/aigc-home/app/calliope-web/src/lib/i18n/es.ts
|
||||
332522881a6fc1ea9d227745f681ce484ae75367d56e61bcdad13a17487f3ce3 /opt/aigc-home/app/calliope-web/src/lib/i18n/fr.ts
|
||||
b2c9d3b530fe388781f6b85dbef4b9c71ff0652a42ad8faf6e7864e8ef5438c2 /opt/aigc-home/app/calliope-web/src/lib/i18n/ja.ts
|
||||
89862053863f268a6f3dff975be041b526ef6b5bb937c9660f97b4e80ce241e8 /opt/aigc-home/app/calliope-web/src/lib/i18n/ko.ts
|
||||
42b332e23e881092977434ee0bd446bb410834de262d96f13e0a7150e0da4bb4 /opt/aigc-home/app/calliope-web/src/lib/i18n/zh.ts
|
||||
60895e55f0860fba53ef9d80981b7ec13bc1d17c0ab5dd7d4aeb5ec930131b76 /opt/aigc-home/app/calliope-web/src/lib/ids.ts
|
||||
8354c545da56aa7d05edba6820614611db5c2846407f9ec69e4d2b0da4e29814 /opt/aigc-home/app/calliope-web/src/lib/jobLabels.ts
|
||||
b17722f478a633982c02191e02ef908297357d33b15d01944f59dce0c13c3ed9 /opt/aigc-home/app/calliope-web/src/lib/jobProgress.ts
|
||||
536733c170cc5266cd1f5d0a0eeda9a2a56c8f8c9613058c32a7a9d103063842 /opt/aigc-home/app/calliope-web/src/lib/jobStatus.ts
|
||||
d57e21fb3549b5c773073cb7d4c9b62bc79c16a9fa24d5d488450940be27ed9e /opt/aigc-home/app/calliope-web/src/lib/media/exportFrame.ts
|
||||
81762ea6ec15806d62298b838c48919744a6431df8a3300e28b6d214892b842e /opt/aigc-home/app/calliope-web/src/lib/media/lastFrame.ts
|
||||
345d9b8f82fb4893ecc2a0866581838e7b5d45e45259c40d40234d915545f929 /opt/aigc-home/app/calliope-web/src/lib/missingOutputs.svelte.ts
|
||||
3975f02ba220365dd32d4053d199800355c80c1a5d2a26942779df304fa042b8 /opt/aigc-home/app/calliope-web/src/lib/promptTemplates.ts
|
||||
57900f9c597c868a40ff6d142ef3d3b0e3ef364f4adddd967414688ecfbd7293 /opt/aigc-home/app/calliope-web/src/lib/reduceMotion.svelte.ts
|
||||
abd820a9a7a47f8eede494583eaa18b5f34b1abfe73260a6fb311f2e8ea01494 /opt/aigc-home/app/calliope-web/src/lib/referenceAgent.ts
|
||||
c037acebc7b878e00f680e65c5bfd6934b9eb8f7cc2e7d8c58afc4c87f93a927 /opt/aigc-home/app/calliope-web/src/lib/shot/api.ts
|
||||
7495bcb0d98e4dd91ad72c132b31724d97f226ae0420f862a5791d20888e2526 /opt/aigc-home/app/calliope-web/src/lib/shot/calibration/compositionPresets.ts
|
||||
e9301e261d4a12184f820ee0b0a547bf8c061bb65c58ee2a1967d89537b0c9b2 /opt/aigc-home/app/calliope-web/src/lib/shot/calibration/shotAxes.ts
|
||||
24ebe82dc55cc53cf3dfe0bc40275a9613353dc9116539acc6faf0b0aa597d80 /opt/aigc-home/app/calliope-web/src/lib/shot/calibration/shotSolver.ts
|
||||
5de1596d7827248cd5be5e576a54848d18b63225061beeccd2bd144ed678a2ea /opt/aigc-home/app/calliope-web/src/lib/shot/components/CaptureStrip.svelte
|
||||
d8450affdb98ce82325f9b0f860ca1f5493a0404c4f2dc52792758f25e7e145e /opt/aigc-home/app/calliope-web/src/lib/shot/components/ScenePanel.svelte
|
||||
abbc4622691782aa8ccddd55e43324642d715f2c5f78d32ef4f8d6ad1c86be6f /opt/aigc-home/app/calliope-web/src/lib/shot/components/ShotInspector.svelte
|
||||
810a24fb53b6458747ca981039fcde44a4b2f3b89242ec6f2be96e5f11e1d95d /opt/aigc-home/app/calliope-web/src/lib/shot/components/ShotSessionRail.svelte
|
||||
6c6b55ab12443cb7086a41448c2245c526784af60df5b8d3078f046de5cdc574 /opt/aigc-home/app/calliope-web/src/lib/shot/components/ShotViewport.svelte
|
||||
cc871911957ac769829dd487095488e0cf4cdbf44f99ace78df906fa351ae71c /opt/aigc-home/app/calliope-web/src/lib/shot/components/TimelineStrip.svelte
|
||||
6b18ef874ccd3c3508ac35788a8b9a1e924a79b4a7af9f7b9373e4f0b383e7e3 /opt/aigc-home/app/calliope-web/src/lib/shot/components/ViewportToolbar.svelte
|
||||
7de37792057307fd93a2df295308ccef609d9ec16ed17793eb8e2faf2295c626 /opt/aigc-home/app/calliope-web/src/lib/shot/helpers/cameraUtils.ts
|
||||
2a07e4133ccc93420b64fcde15c43638219359cf42b1c8d9c849c44eb85cd2fe /opt/aigc-home/app/calliope-web/src/lib/shot/helpers/jointConfig.ts
|
||||
a64e0938354f7363195285dfc564949b3dfac5f0be254177ce1378fdc00bea59 /opt/aigc-home/app/calliope-web/src/lib/shot/helpers/mannequinFactory.ts
|
||||
0f94e29cb1d0b69d785dfaa326cee6a993c60472328f16664a5291d7e6caecf8 /opt/aigc-home/app/calliope-web/src/lib/shot/helpers/mirror.ts
|
||||
d6569792a3a9e2dbff0f82b527e4d3b9d35874864c019f5dbc7aa8e4da947b25 /opt/aigc-home/app/calliope-web/src/lib/shot/helpers/posture.ts
|
||||
18385f2f998666e864d5b67b6531b85e6d42c0d6de021a261f11199cb78b2fec /opt/aigc-home/app/calliope-web/src/lib/shot/helpers/primitiveFactory.ts
|
||||
480d36f1dabed4944b34471ed55d869e28f6e81875e6558c62a0b75d694cb783 /opt/aigc-home/app/calliope-web/src/lib/shot/motion/sampleTrack.ts
|
||||
7af90bd8c957299c282e12fca917f8fa56d2069b4ded99cc4798d1e730506f1c /opt/aigc-home/app/calliope-web/src/lib/shot/shotStore.svelte.ts
|
||||
f17a0e08c4c42336a8983f46f4970896daee72577e38fd4739f322d99c20e27d /opt/aigc-home/app/calliope-web/src/lib/shot/types/mannequin-js.d.ts
|
||||
d9c276a046c0ba7326d65293a56711913b8be107439470342220e98c6736af0b /opt/aigc-home/app/calliope-web/src/lib/skillFrontmatter.ts
|
||||
0a2075b0f882d9b354ed3d7a0a8913d9d9f2e0e380ad8ea4f655fc89f1986d97 /opt/aigc-home/app/calliope-web/src/lib/storage.ts
|
||||
07a9b47b8d793c23bc490b23d4bc7e4e5083e3780851525edc60c92118c43716 /opt/aigc-home/app/calliope-web/src/lib/toast.ts
|
||||
baa828f60b258b06032da3a2d289d353e1ba61b00bc3b06852d184936663d3ee /opt/aigc-home/app/calliope-web/src/lib/wideLayout.ts
|
||||
363320f14113146f617b12ead186db995fa99de71ad04be336ea1cb3e6f28453 /opt/aigc-home/app/calliope-web/src/routes/+layout.svelte
|
||||
c9a6362f526374fe88546908b976f897bccc8bb1b12a3797e176afc0d37a1172 /opt/aigc-home/app/calliope-web/src/routes/+layout.ts
|
||||
6c6481bf7b09e2e74400a112b66ad2430be6cdc843ebdc1c20d370bdacc174d8 /opt/aigc-home/app/calliope-web/src/routes/+page.svelte
|
||||
82665e45d52d9b8f182fe7395e4c8b2726beb62320de2f581b5b73d9c9d3987f /opt/aigc-home/app/calliope-web/src/routes/+page.ts
|
||||
bce9c599232e064eb946d9090391066ee8adcdc269797b8a874cca8effa4427d /opt/aigc-home/app/calliope-web/src/routes/agents/+page.svelte
|
||||
58ca28942740b2ff08d6416473a2e8038f8a660017ce0b8b0d9ab3bb44453659 /opt/aigc-home/app/calliope-web/src/routes/agents/+page.ts
|
||||
9949bca2d32eac10b2f9dfe91d0f6b539205259fa15b0a4c6443a6c1e600dc35 /opt/aigc-home/app/calliope-web/src/routes/app.css
|
||||
e7212b5a4b2eedcd6f107e5fcfb5ae8a2ded5bce3d12f7c279f6dfa4c0007660 /opt/aigc-home/app/calliope-web/src/routes/build-scene/+page.svelte
|
||||
f21b64d615dfbac548e7298fe8d130dfc7e0e148ffb00d6945ac470da791e002 /opt/aigc-home/app/calliope-web/src/routes/build-scene/+page.ts
|
||||
370b52d8e87153b57ef73eed1846a770eaf9b7c356b310a3fdcc8d3c295a403e /opt/aigc-home/app/calliope-web/src/routes/canvas/+page.svelte
|
||||
f21b64d615dfbac548e7298fe8d130dfc7e0e148ffb00d6945ac470da791e002 /opt/aigc-home/app/calliope-web/src/routes/canvas/+page.ts
|
||||
1e0197911d9752767374ca1fa1157ea534f24553fbea1e9db5fde0294af4f034 /opt/aigc-home/app/calliope-web/src/routes/canvas/[id]/+layout.svelte
|
||||
23de1dcb2b94ba7050764b7bfa04fd447aa36f50241f867783646ccb669a8658 /opt/aigc-home/app/calliope-web/src/routes/canvas/[id]/+page.svelte
|
||||
f21b64d615dfbac548e7298fe8d130dfc7e0e148ffb00d6945ac470da791e002 /opt/aigc-home/app/calliope-web/src/routes/canvas/[id]/+page.ts
|
||||
28c91d13beac10eba7964e562e90c8c4a3ec2652e69710970cd4733e9d42e1e5 /opt/aigc-home/app/calliope-web/src/routes/connections/+page.svelte
|
||||
bc76b47b593f0ae5e6946df26524d70e46b939889ccec090dc27a47cf71af140 /opt/aigc-home/app/calliope-web/src/routes/image/+page.svelte
|
||||
743d590d5e52e74a5b209448759c603c5b1ee5ae6540916354d496820c7acd7f /opt/aigc-home/app/calliope-web/src/routes/library/+page.svelte
|
||||
58ca28942740b2ff08d6416473a2e8038f8a660017ce0b8b0d9ab3bb44453659 /opt/aigc-home/app/calliope-web/src/routes/library/+page.ts
|
||||
5684b7f04e3eef40f349a5e98b22c6ebb05c9fa75bb4606a988b0f2ee65cf13a /opt/aigc-home/app/calliope-web/src/routes/models/+page.svelte
|
||||
58ca28942740b2ff08d6416473a2e8038f8a660017ce0b8b0d9ab3bb44453659 /opt/aigc-home/app/calliope-web/src/routes/models/+page.ts
|
||||
9f29a23ba5aed307d4f28c7091723269e6583d53357805f7e8893d468832fc53 /opt/aigc-home/app/calliope-web/src/routes/playground/+page.svelte
|
||||
f21b64d615dfbac548e7298fe8d130dfc7e0e148ffb00d6945ac470da791e002 /opt/aigc-home/app/calliope-web/src/routes/playground/+page.ts
|
||||
79812eaac33b12b43f539f0b957ebe4aeb64cf80eac03741ebfd4ba1cbf76335 /opt/aigc-home/app/calliope-web/src/routes/project/[id]/+page.ts
|
||||
f6ba8f210c2b8db20d401c57b6c84119ebda467b15ad9e41e8ba5ae89477354d /opt/aigc-home/app/calliope-web/src/routes/projects/+page.ts
|
||||
675613be31a3a5f95cb8675a42790425f4f864380d6b3c141ab028f5069fc9fa /opt/aigc-home/app/calliope-web/src/routes/settings/+page.svelte
|
||||
58ca28942740b2ff08d6416473a2e8038f8a660017ce0b8b0d9ab3bb44453659 /opt/aigc-home/app/calliope-web/src/routes/settings/+page.ts
|
||||
3c0f1d1b8d92c7b3e6372a83aaf5b3318442d135eb0de267cebfefb795d8ae49 /opt/aigc-home/app/calliope-web/src/routes/tasks/+page.svelte
|
||||
f2a395573851fe6916670ac392bedceaa5f93312c6560a33470b661e80132329 /opt/aigc-home/app/calliope-web/src/routes/video/+page.svelte
|
||||
32aa5a552f98d3a2ee487ed236de09daea24bd33ccf97f48bbe1f29fc639abbe /opt/aigc-home/app/calliope-web/src/routes/workbench/+page.svelte
|
||||
58ca28942740b2ff08d6416473a2e8038f8a660017ce0b8b0d9ab3bb44453659 /opt/aigc-home/app/calliope-web/src/routes/workbench/+page.ts
|
||||
31c0251a0f09a0540021a621410c084ff4d0b1fb9d409de0e461fde224f858db /opt/aigc-home/app/calliope-web/src/routes/workbench/[id]/+page.svelte
|
||||
58ca28942740b2ff08d6416473a2e8038f8a660017ce0b8b0d9ab3bb44453659 /opt/aigc-home/app/calliope-web/src/routes/workbench/[id]/+page.ts
|
||||
bc6dc994baaaf02c80ad390cb71965f2f15422704fa64c81f5b2c31f33b2ab13 /opt/aigc-home/app/calliope-web/src/routes/workflows/+page.svelte
|
||||
f21b64d615dfbac548e7298fe8d130dfc7e0e148ffb00d6945ac470da791e002 /opt/aigc-home/app/calliope-web/src/routes/workflows/+page.ts
|
||||
294450c3d45d4820d0bbf7f1c6e69aa0f3686efb6e3dbcdacf13e34e2d07b35e /opt/aigc-home/app/calliope-backend/src/calliope/routers/workbench.py
|
||||
acd3ba6e0ab29b92f5133358398f3544baa0ce2bea03560004a42e812b6abc8a /opt/aigc-home/app/calliope-backend/src/calliope/static/index.html
|
||||
acd3ba6e0ab29b92f5133358398f3544baa0ce2bea03560004a42e812b6abc8a /opt/aigc-home/releases/stage-HEL665-v5-35fd07a-20261015/build/index.html
|
||||
@@ -0,0 +1,79 @@
|
||||
660a6de4471ec76986e56f8d4467e4ed85a3ee5fdfc47f438410131c0f47f051 /opt/aigc-home/app/calliope-backend/src/calliope/__init__.py
|
||||
b00189efb92d1f8f77fdd9b912e7357bd6a7baaff998f364d55aa0029f7f9417 /opt/aigc-home/app/calliope-backend/src/calliope/agent/__init__.py
|
||||
7d0a16a798d89d7c8932c63698b8f4af91a61f7ec22b29f9e2efde75f06890b9 /opt/aigc-home/app/calliope-backend/src/calliope/agent/agents_registry.py
|
||||
f3ae5d869628d1cf2347e6b216d60dc7d4ea4fb82945fb348180bd04aca1b5d2 /opt/aigc-home/app/calliope-backend/src/calliope/agent/asset_agent.py
|
||||
45daa038f5dbfca19ec351e2e2c4480ed8abc3fa51adf62b46dff714221afe45 /opt/aigc-home/app/calliope-backend/src/calliope/agent/continuity.py
|
||||
300f043668e003bdf98f73cce40ba0251d94356092e5ad1aafe29ee1d4c97e22 /opt/aigc-home/app/calliope-backend/src/calliope/agent/coverage_agent.py
|
||||
2245f0e117e728382554155c8f3e93f5304913c595cf070d9ad29daafb9bd088 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/__init__.py
|
||||
077cebb46d3ecccc1336b9c92a5c82ad21cdd57e55644df9298a5495374bc09d /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/log.py
|
||||
c9429604ecde92dfc95f463fcfbbf2dd0153d91fd3155696238a1713a2cb8dd6 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/loop.py
|
||||
5233ad11b93f072d456861829d0f856436a9f330c2e4870aa2dad15e3bfc117a /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/orchestrator.py
|
||||
f9e2f2f5ab4084c53866621e9c17eb388fd6dd9b4cb06be6c067ee03490f4da5 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/plugins/__init__.py
|
||||
ab17c10c8e372ff94659047dd94b56bdeecf79aa88bce60821538ab4d5e4d875 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/plugins/canvas.py
|
||||
1e7d44d9c9d8031bca9d3537b2d96d05bf9087e64a3ff6a33eee76c09001157e /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/plugins/comfy_mcp.py
|
||||
03c9041e6cc8c443e56d2e0e09effa15107ad7b6bc4048e6b56fdfccd623a2d7 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/plugins/interaction.py
|
||||
7fe0366e33a1600766914a0b7bcc2bc0968bb39850370dec35697055ba8e8c1a /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/plugins/memory.py
|
||||
58cc3518674cd689eade1c2d401d3fb943c5f2429af66da537b6b55ed00d95df /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/plugins/render.py
|
||||
15d479331934ed05c23e19f24b131e566ef528550ab9720fd570d4b1bf3e977b /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/plugins/script.py
|
||||
e9f736490d5ef6951ee0ce2ace3516bc2f6902c6fa3b1ef6ea9b6e7123f5f8d1 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/plugins/shot_builder.py
|
||||
ca11d12eb274a6852af2d091dbe453df582543cfb2d03051528bc629c8f0d024 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/plugins/skills.py
|
||||
8c49aea52a52a59e31269c8e79c988f4a1b3d676c23040f6506fab94789e4a20 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/plugins/story.py
|
||||
5dc7f0e6db53eaebe523b1b22b0e77ef06683c9182a52be7ab54f3c8d9d66395 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/plugins/system.py
|
||||
2a2d4606539c42f2f3acb7b93c73f571b66f53d4ead934bb1d47547825226978 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/plugins/workspace.py
|
||||
7001d912ae819bd25a27db28cffb77b62c53ec28eae9bef320d46d967d195a81 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/policy.py
|
||||
09e935288ad6c5c3579c26e948beac6d14df74978334fe4ffa4b000e8a600edb /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/prompts.py
|
||||
25e0716d36a6598fe516d2d4838cfe54eedf00f390385ede5c252edc3a4a94c0 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/registry.py
|
||||
0495ee7f9ffefe13256688e47b23f68d6361bbef527b7ed4a5bc7f4a8998e1b8 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/runner.py
|
||||
90405bbb07c7cbe1283ead1ae901c8183756a93667e71f4ce585c0a543853198 /opt/aigc-home/app/calliope-backend/src/calliope/agent/harness/tools.py
|
||||
5d225d80e8886e04e9b02d18b0bae358b65f4e66beec1280c4f500e761b1d752 /opt/aigc-home/app/calliope-backend/src/calliope/agent/llm.py
|
||||
beadb0e82e8eb47356f81d81ff41271da97cc51c90707c89ee2908ed095d0380 /opt/aigc-home/app/calliope-backend/src/calliope/agent/prompts.py
|
||||
feffad04e8a668d4426fc6f9021213a9d1122221e685aa5ba30bcaa9611faf45 /opt/aigc-home/app/calliope-backend/src/calliope/agent/script_agent.py
|
||||
483c6658b7573382f3a83add57b9f7e27e83ff1388bf0b900837f81c2b47c56e /opt/aigc-home/app/calliope-backend/src/calliope/agent/skills_store.py
|
||||
2caa50c0e246b542f331c7a52778b5c13331cf3623a431c14bbdc4362674f138 /opt/aigc-home/app/calliope-backend/src/calliope/agent/video_agent.py
|
||||
1d3488bcc2eadcd8a8f2e84ab4f6e9bdf52096b8af637c4da399db041d5a0543 /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/__init__.py
|
||||
e6faad052e3139d921936c7a73e2ee7fb55a3bb3c9fc256535dca5fa6f01ac14 /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/client.py
|
||||
f8bf4d1b91d959044de1dbc4aeb11bf0bb521f51f1928d36a46c670381a08ed7 /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/dry_run.py
|
||||
fc99a623850b581ae38843bbc3baf1b71ca9fd633e6103a34cb809a345e5a647 /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/endpoints.py
|
||||
da82aad77fa561fea5fd6bdb553feaff61ce753a890eea2caa146a5813981952 /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/lint.py
|
||||
12848b11ca346ee3c7703becb96469609b1b68612405a7be3cb7e8fca04291b5 /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/metadata.py
|
||||
460d9463dabc8b055d1bdd81e5b908ad8bfd198da1560e3fe0f859c9cd1dfdf6 /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/output_map.py
|
||||
d300c6f33f30bc9a1d9a4c5163b3a2f71795f9e349849205ec42f85f3a202824 /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/parser.py
|
||||
8ac1417f0f78d952cdb219b10811a6ed7d4ce3bb0d561708858c4a8f5d15b45d /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/patcher.py
|
||||
6629961d33204cbe5b3fc3d8a5a0a4de0c86d235e73f542d191429cf7f4bd2ec /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/preflight.py
|
||||
4ca33343fc3f20e0fb82558cb428548f6415af04fb90213a694fb759515732f2 /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/profiles.py
|
||||
21b024f44a3cba96db1b95a4c3a82591e6af921a31756378970da506ca0f9acf /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/registry.py
|
||||
355aca29ad4283f3cc600181fc0397cd06f8a63d3fe537b6fd10fc92bfcbe5a9 /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/roles.py
|
||||
adc34e80cca7a2b38cc834a713c9e64001caac84413af288389e5554e304bab6 /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/smart_fill.py
|
||||
ebb91f0d291b042d74a44019cfd69dcd553b0f0f96277b7795e52d4dbb7201d1 /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/ui_convert.py
|
||||
db00a79a92d0add6ea3c2509d629c36cbe457c667e388fff50eeec7158d5b9d8 /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/ui_widget_orders.py
|
||||
c27fca69a604705b06809fdd692975ca0e7a08d05d558172962f8910a6a0491d /opt/aigc-home/app/calliope-backend/src/calliope/comfyui/validate.py
|
||||
94cda57274c512feac82ab3c97d9c680abf6706009727d6a6791cef690994001 /opt/aigc-home/app/calliope-backend/src/calliope/config.py
|
||||
9c404874ccbf1d3d49ab509b112f52ecd6a037488f3b0e05e5a08c5ad74123dd /opt/aigc-home/app/calliope-backend/src/calliope/db.py
|
||||
4b3f2620e25c17bf9587beda3b5e73284bf83c6b5fb6beaf1c6e73a73dc560f3 /opt/aigc-home/app/calliope-backend/src/calliope/events/__init__.py
|
||||
a9be4db2e810135a3482432316925b0bed8b2fab9e1395333831b7f5246f987b /opt/aigc-home/app/calliope-backend/src/calliope/events/bus.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 /opt/aigc-home/app/calliope-backend/src/calliope/export/__init__.py
|
||||
cabc42a31999fa1e185b07c74f09b303f793a809127ce6039547c86657d9491b /opt/aigc-home/app/calliope-backend/src/calliope/export/runner.py
|
||||
ee776991e7fbeef9d0a370a734bb9428e015ef9f18b0cbf8f3a8e772614922d9 /opt/aigc-home/app/calliope-backend/src/calliope/main.py
|
||||
ff156ab234a8ea57a120e31c83baa591e613fa776d74cf29f2e75468bcff4e71 /opt/aigc-home/app/calliope-backend/src/calliope/models/__init__.py
|
||||
81bbb9b938d452f41369982b7434520d173ff68b0242278c602f5d489c00527a /opt/aigc-home/app/calliope-backend/src/calliope/models/schemas.py
|
||||
e4a4676265849f0dce021abccc1e7cc9ae6605a60eac8cc7e0a47246ff56bfa8 /opt/aigc-home/app/calliope-backend/src/calliope/poses.py
|
||||
dbe2691f035386e14a3be26725cfba67baf7f6f712fc0061740ea920a05d9f4c /opt/aigc-home/app/calliope-backend/src/calliope/queue/__init__.py
|
||||
22724a06ad9d6fced7572d52392cf2d22c793614097dbab42be9c1bc373b2ab0 /opt/aigc-home/app/calliope-backend/src/calliope/queue/manager.py
|
||||
ee3e91bf7e330ff1981aece7fc4bb475acfd71153ab59db99736ac765a57ce2d /opt/aigc-home/app/calliope-backend/src/calliope/queue/worker.py
|
||||
1805a0888c8dc007500cc1e24030a03555c6619e6fa56f937844831995cba5b8 /opt/aigc-home/app/calliope-backend/src/calliope/queue/workflow_test.py
|
||||
cbacc8761833b64aa017a3df2d933c3897f9f19997044efaea49f206ec1b205a /opt/aigc-home/app/calliope-backend/src/calliope/routers/__init__.py
|
||||
780483a9389c939d632a79e248e58053e0c468f65a27b0a90337412cc0fe1ec8 /opt/aigc-home/app/calliope-backend/src/calliope/routers/agent.py
|
||||
315b6318864370449c3570ae705046501f696ac86cb370de189b05be26d79a05 /opt/aigc-home/app/calliope-backend/src/calliope/routers/assets.py
|
||||
a05c24f6c26d8a8432e8c73deb8acfb1e71fa4f611c10e80ba19dd6c10985e9d /opt/aigc-home/app/calliope-backend/src/calliope/routers/canvas.py
|
||||
6195a37ef48e80a7f423fefcc2b2939c2270542ca4ce81ef58ca06da356b312d /opt/aigc-home/app/calliope-backend/src/calliope/routers/comfyui.py
|
||||
be7f140608380aa93d23893ba97b222fcf5b2a288ca81e5ccb53731875d2cdff /opt/aigc-home/app/calliope-backend/src/calliope/routers/events.py
|
||||
45c1952d866f76ab84cbc221541250ad6e9ae79d06d4e7c7cb4176d395f9d196 /opt/aigc-home/app/calliope-backend/src/calliope/routers/jobs.py
|
||||
3827af29c2645baee4a9ccbbc1d77155e830585bd033955b061bc320b6fb7de3 /opt/aigc-home/app/calliope-backend/src/calliope/routers/library.py
|
||||
ce4f8f21cff507cafdeece1f9427b02172c550da53327095167964cb97ca2145 /opt/aigc-home/app/calliope-backend/src/calliope/routers/playground.py
|
||||
c36d32df6571ecb1bf3bd17be48e47c0cc3864b79db51d764dcf95befbe78383 /opt/aigc-home/app/calliope-backend/src/calliope/routers/projects.py
|
||||
398cf96846e60c8e60907121b4a21f9086c3b03f86e0ad2ad4082acaa9fb4678 /opt/aigc-home/app/calliope-backend/src/calliope/routers/scenes.py
|
||||
a4fedd5c2285ffa0d28b4176e9014d46c27d83b4150a81b2de63ef9b921865cd /opt/aigc-home/app/calliope-backend/src/calliope/routers/settings.py
|
||||
bd235fde191c512e0592e269c8d1c8d8784ef8bce07de6d62a93626fac249744 /opt/aigc-home/app/calliope-backend/src/calliope/routers/shots.py
|
||||
64b3b8e96beaf6b5fa50114188f5f101c09794dcfc5b47ce79174f43445b4bbf /opt/aigc-home/app/calliope-backend/src/calliope/routers/story.py
|
||||
294450c3d45d4820d0bbf7f1c6e69aa0f3686efb6e3dbcdacf13e34e2d07b35e /opt/aigc-home/app/calliope-backend/src/calliope/routers/workbench.py
|
||||
455073355ada5eb2807f90496ff3a28b3e1f9f06bbc9e262a8da354d6aec15ae /opt/aigc-home/app/calliope-backend/src/calliope/routers/workflows.py
|
||||
@@ -0,0 +1,49 @@
|
||||
{"image_wf": 1, "video_wf": 2, "endpoint": "e9c0ef15-ab9a-4956-81d4-2d42425feea2"}
|
||||
{'dual': 3, 'h3': 4, 'project': 1}
|
||||
RUN e2e/hel728.cjs
|
||||
PASS 默认主题为夜片场
|
||||
PASS night 保留原编辑组件和正反向草稿
|
||||
PASS day 保留原编辑组件和正反向草稿
|
||||
PASS grade 保留原编辑组件和正反向草稿
|
||||
PASS bluehour 保留原编辑组件和正反向草稿
|
||||
PASS 刷新恢复主题和草稿
|
||||
PASS 方向键移动主题选项焦点
|
||||
PASS Esc 关闭并恢复入口焦点
|
||||
PASS 创作台兼容入口保留全部参数及片段
|
||||
PASS 隔离真实队列已收到任务
|
||||
PASS night 上传素材和运行中编辑状态保留
|
||||
PASS day 上传素材和运行中编辑状态保留
|
||||
PASS grade 上传素材和运行中编辑状态保留
|
||||
PASS bluehour 上传素材和运行中编辑状态保留
|
||||
PASS 切主题后原任务完成且产物归属不变
|
||||
PASS 切主题保留视频节点和暂停状态
|
||||
PASS 切主题保留智能体任务输入组件和草稿
|
||||
PASS 切主题保留未保存设置
|
||||
PASS 1440,900 night 导航入口和四主题完整可见
|
||||
PASS 1440,900 day 导航入口和四主题完整可见
|
||||
PASS 1440,900 grade 导航入口和四主题完整可见
|
||||
PASS 1440,900 bluehour 导航入口和四主题完整可见
|
||||
PASS 1366,768 night 导航入口和四主题完整可见
|
||||
PASS 1366,768 day 导航入口和四主题完整可见
|
||||
PASS 1366,768 grade 导航入口和四主题完整可见
|
||||
PASS 1366,768 bluehour 导航入口和四主题完整可见
|
||||
PASS 1280,720 night 导航入口和四主题完整可见
|
||||
PASS 1280,720 day 导航入口和四主题完整可见
|
||||
PASS 1280,720 grade 导航入口和四主题完整可见
|
||||
PASS 1280,720 bluehour 导航入口和四主题完整可见
|
||||
PASS 模型连接入口保留页签参数
|
||||
PASS 旧项目链接保留阶段参数
|
||||
PASS 全局搜索保留 视频创作
|
||||
PASS 全局搜索保留 大语言模型
|
||||
PASS 全局搜索保留 试验台
|
||||
PASS 全局搜索保留 三维搭景
|
||||
PASS night 首次绘制前恢复主题及底色
|
||||
PASS day 首次绘制前恢复主题及底色
|
||||
PASS grade 首次绘制前恢复主题及底色
|
||||
PASS bluehour 首次绘制前恢复主题及底色
|
||||
PASS 存储被禁用仍可切换且显示提示
|
||||
PASS 存储被禁用保留输入
|
||||
PASS 无效存储回退夜片场
|
||||
PASS 浏览器无运行错误
|
||||
TOTAL 44
|
||||
FAILED SCRIPTS []
|
||||
@@ -0,0 +1,47 @@
|
||||
# HEL-728 B0 自测与已知限制
|
||||
|
||||
## 通过
|
||||
|
||||
- `npm run check`:0 错误;1 个第五版已存在的视频字幕警告。干净第五版副本同为 0 错误、1 警告。
|
||||
- `npm test`:存储 SSR 安全、局域网随机 ID、技能正文、视频尾帧算法四组既有校验全部通过。
|
||||
- `npm run build`:静态构建成功;`git diff --check` 通过。
|
||||
- 本批全部修改的前端文件:Prettier 检查通过。
|
||||
- `e2e/hel728.cjs`:**44 项通过、0 个浏览器运行错误**。在一次性本地后端、假 ComfyUI、假 LLM 上检查;真实 HTTP 上传首尾帧和队列生成,四主题之间切换时原编辑 DOM 节点、正反向草稿、已上传文件、运行中的任务 ID/工作流归属不变,完成后仍可显示视频且默认暂停。另检查智能体输入、未保存设置、刷新恢复、方向键/Enter/Esc 与焦点返回、非法/禁用存储、旧项目链接及完整 URL 来源上下文。
|
||||
- 框架脚本被阻止时检查四个主题:head 脚本与阻塞样式表独立恢复主题与正确页面底色,没有依赖框架完成后才着色。
|
||||
- 人工查看 1440×900、1366×768、1280×720 截图:主题卡片完整可见、侧栏不溢出、浅色文字与面板正常。截图采用减少动效模式消除过渡帧;产品开关与系统减少动效沿用既有机制。
|
||||
|
||||
## 存量失败及原版对照
|
||||
|
||||
本批未修改 `calliope-backend`:`git diff --quiet 35fd07a -- calliope-backend` 退出 0。
|
||||
|
||||
后端全量 `pytest -q`:**834 通过、5 失败、6 跳过**。在 git archive 导出的干净 `35fd07a` 副本中,用相同解释器/依赖和 `PYTHONPATH=src` 单独复跑这五项,全部复现:
|
||||
|
||||
1. `test_agent_comfy_mcp.py::test_comfy_server_info_scoping`:`agent_events` 表不存在。
|
||||
2. `test_continuity.py::test_h3_messages_bind_neighbor_shots_and_keep_file_slots`:Linux 上 Windows 路径的名称提取与断言不符。
|
||||
3. `test_h3_profile.py::test_resolve_h3_references_prefers_form_files_over_story`:Linux 上 Windows 路径的名称提取与断言不符。
|
||||
4. `test_scene_settings.py::test_preview_prompt_uses_form_references_not_story_cast`:同类 Windows 路径名称问题。
|
||||
5. `test_script_scene_count.py::test_script_regenerate_keeps_expanded_count`:重新生成剧本返回 500,断言预期 200。
|
||||
|
||||
全仓 `npm run lint`:原版 68 个文件的 Prettier 失败,本批剩余 63 个,均为未修改文件;ESLint 本身缺少 v9 配置文件,原仓未提供。没有为本批新增独立 lint 规则或格式覆盖。完整输出附在本目录。
|
||||
|
||||
现有两份端到端脚本均在本批和干净第五版副本中失败,未删断言或改正式功能迎合旧测试:
|
||||
|
||||
- `e2e/reference/check.cjs`:前 8 项通过,随后等待第五版已移除的 `.result-item` 历史栏超时;当前 UI 用作品画布与独立生成历史弹层。
|
||||
- `e2e/reference/check-copy-fallback.cjs`:等待旧版「编辑内容并带回项目」按钮超时;当前第五版已重排成果编辑流程。
|
||||
|
||||
这些脚本没有被本批修改;原版和本批失败日志都保留。不能据此宣称现有端到端全绿。
|
||||
|
||||
## 运行方式
|
||||
|
||||
安装仓库锁定依赖后,从仓库根目录运行:
|
||||
|
||||
```sh
|
||||
# 指向环境已有的 Playwright 和 Chromium;按环境需要提供系统库路径。
|
||||
PLAYWRIGHT_MODULE_PATH=/path/to/playwright-core \
|
||||
CHROMIUM_EXECUTABLE_PATH=/path/to/chromium \
|
||||
HEL728_TESTS=e2e/hel728.cjs python3 e2e/run-hel728.py
|
||||
```
|
||||
|
||||
runner 检查端口空闲,保留上次一次性数据/证据到带时间戳的目录,再启动自己的本地服务;结束时停止自己创建的进程组。默认还会运行两份旧脚本并报告失败。用 `HEL728_WEB_DIR` 可将前端改为干净基线副本,完成同环境对照。
|
||||
|
||||
未验证真实 GPU/付费模型的生成质量;没有操作测试站用户资料或执行真实收费生成。B0 只验证主题与导航地基,B1–B5 的新页面布局、通用存入项目及最终全站动效不在本次完成范围。
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Assemble measured HEL-743 search/header crops, sample left and actual right."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
from PIL import Image, ImageDraw
|
||||
root=Path(__file__).resolve().parents[1]
|
||||
out=root/'e2e/shots/hel743'
|
||||
rows=json.loads((out/'results.json').read_text())['measurements']
|
||||
for width in (1440,1366,1280):
|
||||
entries=[r for r in rows if r['width']==width]
|
||||
panel_width=1040
|
||||
canvas=Image.new('RGB',(panel_width*2,4*164),(35,36,40))
|
||||
draw=ImageDraw.Draw(canvas)
|
||||
for index,r in enumerate(entries):
|
||||
for column,side in enumerate(('sample','actual')):
|
||||
x=column*panel_width+10;y=index*164+8;m=r[side]
|
||||
draw.text((x,y),f"{width} / {r['theme']} / {side} | head {m['head']['width']}x{m['head']['height']} | input {m['search']['width']} | file {m['file']['width']}",fill='white')
|
||||
im=Image.open(out/f"{width}-{r['theme']}-{side}.png")
|
||||
for key,dy in (('head',26),('box',90)):
|
||||
box=m[key];left=int(box['x']);top=int(box['y']);right=round(box['x']+box['width']);bottom=round(box['y']+box['height'])
|
||||
canvas.paste(im.crop((left,top,right,bottom)),(x,y+dy))
|
||||
canvas.save(out/f'compare-{width}.png')
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
// B0 state preservation and navigation checks on the disposable reference stack.
|
||||
const { chromium } = require(
|
||||
process.env.PLAYWRIGHT_MODULE_PATH || "playwright",
|
||||
);
|
||||
const assert = require("node:assert/strict"),
|
||||
fs = require("node:fs"),
|
||||
path = require("node:path");
|
||||
const out = path.resolve("e2e/shots/hel728"),
|
||||
f = JSON.parse(fs.readFileSync("e2e/shots/reference/fixtures.json"));
|
||||
const WEB = "http://127.0.0.1:5173",
|
||||
API = "http://127.0.0.1:8247";
|
||||
const themes = ["night", "day", "grade", "bluehour"],
|
||||
names = ["夜片场", "日光棚", "调色间", "蓝调时刻"];
|
||||
const checks = [];
|
||||
function ok(name, value) {
|
||||
assert(value, name);
|
||||
checks.push(name);
|
||||
console.log("PASS", name);
|
||||
}
|
||||
async function change(p, id) {
|
||||
await p.getByRole("button", { name: "主题", exact: true }).click();
|
||||
await p
|
||||
.locator(".choice")
|
||||
.filter({ hasText: names[themes.indexOf(id)] })
|
||||
.click();
|
||||
await p.keyboard.press("Escape");
|
||||
await p.waitForFunction(
|
||||
(id) => document.documentElement.dataset.theme === id,
|
||||
id,
|
||||
);
|
||||
}
|
||||
async function waitJob(id) {
|
||||
for (let i = 0; i < 120; i++) {
|
||||
const j = (
|
||||
await fetch(API + "/api/playground/jobs").then((r) => r.json())
|
||||
).find((j) => j.id === id);
|
||||
assert(j, "submitted job remains in queue");
|
||||
if (j.status === "done") return j;
|
||||
assert(!["failed", "cancelled"].includes(j.status), JSON.stringify(j));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
throw Error("job timed out");
|
||||
}
|
||||
(async () => {
|
||||
const browser = await chromium.launch({
|
||||
executablePath: process.env.CHROMIUM_EXECUTABLE_PATH,
|
||||
headless: true,
|
||||
args: ["--no-sandbox"],
|
||||
});
|
||||
const errors = [];
|
||||
const modepath = "e2e/fake_mode.json",
|
||||
saved = fs.existsSync(modepath) ? fs.readFileSync(modepath) : null;
|
||||
try {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1440, height: 900 },
|
||||
}),
|
||||
p = await context.newPage();
|
||||
p.on(
|
||||
"pageerror",
|
||||
(e) => (errors.push(e.message), console.error("PAGEERROR", e.stack)),
|
||||
);
|
||||
await p.goto(WEB + "/image?wf=" + f.dual);
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
ok(
|
||||
"默认主题为夜片场",
|
||||
await p.evaluate(
|
||||
() => document.documentElement.dataset.theme === "night",
|
||||
),
|
||||
);
|
||||
await p.locator("#desk-prompt").fill("切换主题时保留的半篇提示词");
|
||||
await p.locator("#desk-negative").fill("反向草稿");
|
||||
await p
|
||||
.locator("#desk-prompt")
|
||||
.evaluate((e) => (window.__promptElement = e));
|
||||
for (const id of themes) {
|
||||
await change(p, id);
|
||||
ok(
|
||||
id + " 保留原编辑组件和正反向草稿",
|
||||
(await p
|
||||
.locator("#desk-prompt")
|
||||
.evaluate(
|
||||
(e) =>
|
||||
e === window.__promptElement &&
|
||||
e.value === "切换主题时保留的半篇提示词",
|
||||
)) && (await p.locator("#desk-negative").inputValue()) === "反向草稿",
|
||||
);
|
||||
}
|
||||
await p.reload();
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
ok(
|
||||
"刷新恢复主题和草稿",
|
||||
(await p.evaluate(
|
||||
() => document.documentElement.dataset.theme === "bluehour",
|
||||
)) &&
|
||||
(await p.locator("#desk-prompt").inputValue()) ===
|
||||
"切换主题时保留的半篇提示词",
|
||||
);
|
||||
// Keyboard selection and focus return use the existing layered Modal.
|
||||
const trigger = p.getByRole("button", { name: "主题", exact: true });
|
||||
await trigger.click();
|
||||
await p.locator(".choice").first().focus();
|
||||
await p.keyboard.press("ArrowDown");
|
||||
ok(
|
||||
"方向键移动主题选项焦点",
|
||||
await p
|
||||
.locator(".choice")
|
||||
.nth(1)
|
||||
.evaluate((e) => document.activeElement === e),
|
||||
);
|
||||
await p.keyboard.press("Enter");
|
||||
await p.keyboard.press("Escape");
|
||||
ok(
|
||||
"Esc 关闭并恢复入口焦点",
|
||||
await trigger.evaluate((e) => document.activeElement === e),
|
||||
);
|
||||
// All source URL parameters survive the temporary gateway.
|
||||
const q = new URLSearchParams({
|
||||
kind: "video",
|
||||
wf: String(f.h3),
|
||||
from: "workbench",
|
||||
project: String(f.project),
|
||||
stage: "shot",
|
||||
source_kind: "shot",
|
||||
source_id: "s1",
|
||||
label: "镜头一",
|
||||
prompt: "上下文提示词",
|
||||
negative: "上下文反向",
|
||||
duration: "6",
|
||||
mode: "image2video",
|
||||
});
|
||||
await p.goto(WEB + "/create?" + q + "#source");
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
ok(
|
||||
"创作台兼容入口保留全部参数及片段",
|
||||
new URL(p.url()).pathname === "/video" &&
|
||||
[...q].every(([k, v]) => new URL(p.url()).searchParams.get(k) === v) &&
|
||||
new URL(p.url()).hash === "#source",
|
||||
);
|
||||
await p.goto(WEB + "/video?wf=" + f.h3);
|
||||
await p.getByRole("button", { name: "管理素材", exact: true }).click();
|
||||
const first = p
|
||||
.locator(".a-slot")
|
||||
.filter({ has: p.getByText("首帧", { exact: true }) }),
|
||||
last = p
|
||||
.locator(".a-slot")
|
||||
.filter({ has: p.getByText("尾帧", { exact: true }) });
|
||||
await first.waitFor();
|
||||
await first
|
||||
.locator("input[type=file]")
|
||||
.setInputFiles("calliope-web/static/refract/coast.jpg");
|
||||
await last
|
||||
.locator("input[type=file]")
|
||||
.setInputFiles("calliope-web/static/refract/arch.jpg");
|
||||
await first.locator("img").waitFor();
|
||||
await last.locator("img").waitFor();
|
||||
await p.locator("#desk-prompt").fill("上传和进行中任务保留");
|
||||
const sources = [
|
||||
await first.locator("img").getAttribute("src"),
|
||||
await last.locator("img").getAttribute("src"),
|
||||
];
|
||||
await p.getByRole("button", { name: "完成", exact: true }).click();
|
||||
await p.locator("#desk-prompt").evaluate((e) => (window.__videoPrompt = e));
|
||||
fs.writeFileSync(
|
||||
modepath,
|
||||
JSON.stringify({ mode: "ok", delay_sec: 12, videos: true }),
|
||||
);
|
||||
const response = p.waitForResponse(
|
||||
(r) =>
|
||||
r.url().endsWith("/api/playground/generate") &&
|
||||
r.request().method() === "POST",
|
||||
);
|
||||
await p.locator(".gen").click();
|
||||
const job = (await (await response).json()).job;
|
||||
ok("隔离真实队列已收到任务", !!job.id);
|
||||
for (const id of themes) {
|
||||
await change(p, id);
|
||||
ok(
|
||||
id + " 上传素材和运行中编辑状态保留",
|
||||
(await p.locator(".media-token img").nth(0).getAttribute("src")) ===
|
||||
sources[0] &&
|
||||
(await p.locator(".media-token img").nth(1).getAttribute("src")) ===
|
||||
sources[1] &&
|
||||
(await p
|
||||
.locator("#desk-prompt")
|
||||
.evaluate(
|
||||
(e) =>
|
||||
e === window.__videoPrompt &&
|
||||
e.value === "上传和进行中任务保留",
|
||||
)),
|
||||
);
|
||||
}
|
||||
const completed = await waitJob(job.id);
|
||||
ok(
|
||||
"切主题后原任务完成且产物归属不变",
|
||||
completed.workflow_id === f.h3 && completed.output_paths.length > 0,
|
||||
);
|
||||
await p.locator(".artboard video").waitFor();
|
||||
await change(p, "day");
|
||||
ok(
|
||||
"切主题保留视频节点和暂停状态",
|
||||
await p
|
||||
.locator(".artboard video")
|
||||
.evaluate((v) => v.paused && !v.autoplay),
|
||||
);
|
||||
await p.screenshot({ path: path.join(out, "uploaded-video-state.png") });
|
||||
await p.goto(WEB + "/agents");
|
||||
await p.locator(".agent-composer textarea").fill("智能体任务输入草稿");
|
||||
await p
|
||||
.locator(".agent-composer textarea")
|
||||
.evaluate((e) => (window.__agentElement = e));
|
||||
await change(p, "night");
|
||||
ok(
|
||||
"切主题保留智能体任务输入组件和草稿",
|
||||
await p
|
||||
.locator(".agent-composer textarea")
|
||||
.evaluate(
|
||||
(e) =>
|
||||
e === window.__agentElement && e.value === "智能体任务输入草稿",
|
||||
),
|
||||
);
|
||||
// Preserve dirty settings separately from the appearance section.
|
||||
await p.goto(WEB + "/settings");
|
||||
await p.locator("input[type=number]").first().fill("2");
|
||||
await change(p, "grade");
|
||||
ok(
|
||||
"切主题保留未保存设置",
|
||||
(await p.locator("input[type=number]").first().inputValue()) === "2",
|
||||
);
|
||||
await p.getByRole("button", { name: "放弃", exact: true }).click();
|
||||
await p.goto(WEB + "/settings?tab=appearance");
|
||||
await p.locator(".choices").waitFor();
|
||||
await p.emulateMedia({ reducedMotion: "reduce" });
|
||||
for (const size of [
|
||||
[1440, 900],
|
||||
[1366, 768],
|
||||
[1280, 720],
|
||||
]) {
|
||||
await p.setViewportSize({ width: size[0], height: size[1] });
|
||||
for (const id of themes) {
|
||||
await p
|
||||
.locator(".choice")
|
||||
.filter({ hasText: names[themes.indexOf(id)] })
|
||||
.click();
|
||||
ok(
|
||||
`${size} ${id} 导航入口和四主题完整可见`,
|
||||
(await p
|
||||
.locator(".sidebar")
|
||||
.evaluate((e) => e.scrollHeight <= e.clientHeight + 1)) &&
|
||||
(await p
|
||||
.locator(".choices")
|
||||
.evaluate((e) => e.getBoundingClientRect().right <= innerWidth)),
|
||||
);
|
||||
await p.screenshot({
|
||||
path: path.join(out, `${id}-${size[0]}x${size[1]}.png`),
|
||||
});
|
||||
}
|
||||
}
|
||||
await p.goto(WEB + "/model-connections?tab=llm&keep=hello#models");
|
||||
await p.locator(".sidebar").waitFor();
|
||||
ok(
|
||||
"模型连接入口保留页签参数",
|
||||
new URL(p.url()).pathname === "/models" &&
|
||||
new URL(p.url()).searchParams.get("keep") === "hello" &&
|
||||
new URL(p.url()).hash === "#models",
|
||||
);
|
||||
await p.goto(
|
||||
WEB + "/project/" + f.project + "?stage=assets&keep=source#draft",
|
||||
);
|
||||
await p.locator(".sidebar").waitFor();
|
||||
ok(
|
||||
"旧项目链接保留阶段参数",
|
||||
new URL(p.url()).pathname === "/workbench/" + f.project &&
|
||||
new URL(p.url()).searchParams.get("keep") === "source" &&
|
||||
new URL(p.url()).hash === "#draft",
|
||||
);
|
||||
// Explicit legacy tools remain searchable until B1/B2 relocate them.
|
||||
await p.keyboard.press("Control+k");
|
||||
for (const label of ["视频创作", "大语言模型", "试验台", "三维搭景"])
|
||||
ok(
|
||||
"全局搜索保留 " + label,
|
||||
(await p.getByRole("option").filter({ hasText: label }).count()) > 0,
|
||||
);
|
||||
await p.keyboard.press("Escape");
|
||||
// Block framework scripts to verify the remembered theme before hydration.
|
||||
const colours = [
|
||||
"rgb(7, 8, 12)",
|
||||
"rgb(233, 228, 218)",
|
||||
"rgb(27, 27, 28)",
|
||||
"rgb(10, 16, 41)",
|
||||
];
|
||||
for (let n = 0; n < themes.length; n++) {
|
||||
const cold = await browser.newContext();
|
||||
await cold.addInitScript(
|
||||
(id) => localStorage.setItem("aigc-theme", id),
|
||||
themes[n],
|
||||
);
|
||||
const coldPage = await cold.newPage();
|
||||
await coldPage.route("**/*", (route) =>
|
||||
route.request().resourceType() === "script"
|
||||
? route.abort()
|
||||
: route.continue(),
|
||||
);
|
||||
await coldPage.goto(WEB + "/", { waitUntil: "domcontentloaded" });
|
||||
ok(
|
||||
themes[n] + " 首次绘制前恢复主题及底色",
|
||||
await coldPage.evaluate(
|
||||
({ id, colour }) =>
|
||||
document.documentElement.dataset.theme === id &&
|
||||
getComputedStyle(document.documentElement).backgroundColor ===
|
||||
colour &&
|
||||
!document.querySelector(".refract-shell"),
|
||||
{ id: themes[n], colour: colours[n] },
|
||||
),
|
||||
);
|
||||
await cold.close();
|
||||
}
|
||||
const reference = await browser.newPage();
|
||||
await reference.goto(
|
||||
"file://" + path.resolve("design/nocturne/reference.html"),
|
||||
);
|
||||
await reference.evaluate(() => {
|
||||
document.documentElement.classList.add("capture", "reduce");
|
||||
go("settings");
|
||||
});
|
||||
for (const [width, height] of [
|
||||
[1440, 900],
|
||||
[1366, 768],
|
||||
[1280, 720],
|
||||
]) {
|
||||
await reference.setViewportSize({ width, height });
|
||||
await reference.screenshot({
|
||||
path: path.join(out, `reference-${width}x${height}.png`),
|
||||
});
|
||||
}
|
||||
await reference.close();
|
||||
const blocked = await browser.newContext();
|
||||
await blocked.addInitScript(() =>
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
get() {
|
||||
throw new DOMException("blocked", "SecurityError");
|
||||
},
|
||||
}),
|
||||
);
|
||||
const b = await blocked.newPage();
|
||||
b.on(
|
||||
"pageerror",
|
||||
(e) => (errors.push(e.message), console.error("PAGEERROR", e.stack)),
|
||||
);
|
||||
await b.goto(WEB + "/image?wf=" + f.dual);
|
||||
await b.locator("#desk-prompt").fill("禁止存储仍可编辑");
|
||||
await change(b, "day");
|
||||
await b.getByRole("button", { name: "主题", exact: true }).click();
|
||||
ok(
|
||||
"存储被禁用仍可切换且显示提示",
|
||||
await b
|
||||
.getByText("浏览器禁止本地存储:本次有效,其他操作不受影响")
|
||||
.isVisible(),
|
||||
);
|
||||
await b.keyboard.press("Escape");
|
||||
ok(
|
||||
"存储被禁用保留输入",
|
||||
(await b.locator("#desk-prompt").inputValue()) === "禁止存储仍可编辑",
|
||||
);
|
||||
await b.screenshot({ path: path.join(out, "storage-blocked.png") });
|
||||
const invalid = await browser.newContext();
|
||||
await invalid.addInitScript(() =>
|
||||
localStorage.setItem("aigc-theme", "invalid"),
|
||||
);
|
||||
const i = await invalid.newPage();
|
||||
await i.goto(WEB + "/settings?tab=appearance");
|
||||
await i.locator(".choices").waitFor();
|
||||
ok(
|
||||
"无效存储回退夜片场",
|
||||
await i.evaluate(
|
||||
() => document.documentElement.dataset.theme === "night",
|
||||
),
|
||||
);
|
||||
if (errors.length) console.error("BROWSER ERRORS", errors);
|
||||
fs.writeFileSync(
|
||||
path.join(out, "results.json"),
|
||||
JSON.stringify({ checks, errors }, null, 2),
|
||||
);
|
||||
ok("浏览器无运行错误", errors.length === 0);
|
||||
fs.writeFileSync(
|
||||
path.join(out, "results.json"),
|
||||
JSON.stringify({ checks, errors }, null, 2),
|
||||
);
|
||||
console.log("TOTAL", checks.length);
|
||||
} finally {
|
||||
if (saved) fs.writeFileSync(modepath, saved);
|
||||
else if (fs.existsSync(modepath)) fs.unlinkSync(modepath);
|
||||
await browser.close();
|
||||
}
|
||||
})().catch((e) => {
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
+590
@@ -0,0 +1,590 @@
|
||||
// B1 integration: disposable FastAPI -> fake ComfyUI, real UI and HTTP only.
|
||||
const { chromium } = require(
|
||||
process.env.PLAYWRIGHT_MODULE_PATH || "playwright",
|
||||
);
|
||||
const assert = require("node:assert/strict"),
|
||||
fs = require("node:fs"),
|
||||
path = require("node:path");
|
||||
const API = "http://127.0.0.1:8247",
|
||||
WEB = "http://127.0.0.1:5173",
|
||||
out = "e2e/shots/hel729";
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const f = JSON.parse(fs.readFileSync("e2e/shots/reference/fixtures.json"));
|
||||
const checks = [];
|
||||
function ok(n, v) {
|
||||
assert(v, n);
|
||||
checks.push(n);
|
||||
console.log("PASS", n);
|
||||
}
|
||||
async function api(p, b, method) {
|
||||
const r = await fetch(API + p, {
|
||||
method: method || (b ? "POST" : "GET"),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: b ? JSON.stringify(b) : undefined,
|
||||
});
|
||||
if (!r.ok) throw Error(p + " " + (await r.text()));
|
||||
return r.json();
|
||||
}
|
||||
async function wait(fn) {
|
||||
for (let i = 0; i < 120; i++) {
|
||||
const v = await fn();
|
||||
if (v) return v;
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
}
|
||||
throw Error("condition timeout");
|
||||
}
|
||||
function fixtureDimensions(file) {
|
||||
const b = fs.readFileSync(file);
|
||||
return b.readUInt32BE(16) === 480 && b.readUInt32BE(20) === 720;
|
||||
}
|
||||
function mode(video = false, extra = {}) {
|
||||
fs.writeFileSync(
|
||||
"e2e/fake_mode.json",
|
||||
JSON.stringify({
|
||||
mode: "ok",
|
||||
delay_sec: 0,
|
||||
unique_filename: true,
|
||||
videos: video,
|
||||
image_file: path.resolve("calliope-web/static/refract/coast.jpg"),
|
||||
...extra,
|
||||
}),
|
||||
);
|
||||
}
|
||||
async function submit(p, wf, video = false) {
|
||||
mode(video);
|
||||
const before = Math.max(
|
||||
0,
|
||||
...(await api("/api/playground/jobs")).map((j) => j.id),
|
||||
);
|
||||
await p.locator(".gen").click();
|
||||
const j = await wait(async () =>
|
||||
(await api("/api/playground/jobs")).find(
|
||||
(j) => j.id > before && j.workflow_id === wf && j.status === "done",
|
||||
),
|
||||
);
|
||||
await p
|
||||
.locator(".result-title")
|
||||
.filter({ hasText: "第 " + j.id + " 次生成" })
|
||||
.waitFor();
|
||||
return j;
|
||||
}
|
||||
async function shot(p, name) {
|
||||
await p.screenshot({ path: out + "/" + name + ".png" });
|
||||
}
|
||||
(async () => {
|
||||
const browser = await chromium.launch({
|
||||
executablePath: process.env.CHROMIUM_EXECUTABLE_PATH,
|
||||
args: ["--no-sandbox", "--disable-dev-shm-usage"],
|
||||
});
|
||||
const errors = [];
|
||||
let p;
|
||||
const saved = fs.existsSync("e2e/fake_mode.json")
|
||||
? fs.readFileSync("e2e/fake_mode.json")
|
||||
: null;
|
||||
try {
|
||||
const c = await browser.newContext({
|
||||
viewport: { width: 1440, height: 900 },
|
||||
});
|
||||
p = await c.newPage();
|
||||
p.setDefaultTimeout(15000);
|
||||
p.on("pageerror", (e) => errors.push(e.message));
|
||||
await p.goto(WEB + "/image?wf=" + f.dual + "#source");
|
||||
await p.locator("#desk-negative").waitFor();
|
||||
ok(
|
||||
"旧生图入口进入统一创作台并保留工作流和片段",
|
||||
new URL(p.url()).pathname === "/create" &&
|
||||
new URL(p.url()).hash === "#source" &&
|
||||
new URL(p.url()).searchParams.get("wf") === String(f.dual),
|
||||
);
|
||||
await p.locator("#desk-prompt").fill("一位女孩站在海岸,清晨柔光。");
|
||||
await p.locator("#desk-negative").fill("模糊、畸形");
|
||||
await p.locator(".rail-tools input[type=search]").fill("Qwen");
|
||||
ok("检索工作流", (await p.locator(".workflow-pick").count()) === 1);
|
||||
await p.locator(".row-actions button").first().click();
|
||||
ok(
|
||||
"置顶保留输入",
|
||||
(await p.locator("#desk-prompt").inputValue()) ===
|
||||
"一位女孩站在海岸,清晨柔光。",
|
||||
);
|
||||
await p
|
||||
.getByRole("button", { name: "创作设置⌄", exact: true })
|
||||
.first()
|
||||
.click();
|
||||
await p.locator(".modal-panel input[type=number]").first().fill("99");
|
||||
await p.getByRole("button", { name: "取消", exact: true }).click();
|
||||
await p
|
||||
.getByRole("button", { name: "创作设置⌄", exact: true })
|
||||
.first()
|
||||
.click();
|
||||
ok(
|
||||
"参数取消不保存",
|
||||
(await p
|
||||
.locator(".modal-panel input[type=number]")
|
||||
.first()
|
||||
.inputValue()) === "42",
|
||||
);
|
||||
await p.locator(".modal-panel input[type=number]").first().fill("101");
|
||||
await p.getByRole("button", { name: "保存设置", exact: true }).click();
|
||||
const j = await submit(p, f.dual);
|
||||
ok(
|
||||
"真实图片生成写入正反向和参数",
|
||||
JSON.stringify(j.payload).includes("101") &&
|
||||
JSON.stringify(j.payload).includes("模糊、畸形"),
|
||||
);
|
||||
await p.locator("#desk-prompt").fill("这是当前输入,不随历史变化");
|
||||
await p
|
||||
.locator(".history-take")
|
||||
.filter({ hasText: "#" + j.id })
|
||||
.click();
|
||||
ok(
|
||||
"历史状态明确且不修改输入",
|
||||
(await p.locator(".history-banner").isVisible()) &&
|
||||
(await p.locator("#desk-prompt").inputValue()) ===
|
||||
"这是当前输入,不随历史变化",
|
||||
);
|
||||
await shot(p, "1440-history");
|
||||
await p.getByRole("button", { name: "返回当前创作", exact: true }).click();
|
||||
await p.getByRole("button", { name: "展开提示词", exact: true }).click();
|
||||
await p.locator("#desk-prompt").fill("长提示词描述。".repeat(600));
|
||||
ok(
|
||||
"长提示词可展开且生成键可见",
|
||||
(await p.locator("#desk-prompt").evaluate((e) => e.clientHeight > 180)) &&
|
||||
(await p.locator(".gen").isVisible()),
|
||||
);
|
||||
await shot(p, "1440-long-prompt");
|
||||
await p.getByRole("button", { name: "收起提示词", exact: true }).click();
|
||||
await p.locator("#desk-prompt").fill("这是当前输入,不随历史变化");
|
||||
// Previously unseen workflow inherits semantic prompts, returning restores its own parameters.
|
||||
await p.locator(".rail-tools input[type=search]").fill("文生图·回归");
|
||||
await p
|
||||
.locator(".workflow-pick")
|
||||
.filter({ hasText: "文生图·回归" })
|
||||
.click();
|
||||
await p.waitForFunction(() => !document.querySelector("#desk-negative"));
|
||||
ok(
|
||||
"切换新工作流保留提示词",
|
||||
(await p.locator("#desk-prompt").inputValue()) ===
|
||||
"这是当前输入,不随历史变化",
|
||||
);
|
||||
await p.reload();
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
ok(
|
||||
"刷新恢复切换后的工作流及草稿",
|
||||
(await p
|
||||
.locator(".model-selector")
|
||||
.innerText()
|
||||
.then((s) => s.includes("文生图"))) &&
|
||||
(await p.locator("#desk-prompt").inputValue()) ===
|
||||
"这是当前输入,不随历史变化",
|
||||
);
|
||||
await p.locator(".rail-tools input[type=search]").fill("Qwen");
|
||||
await p.locator(".workflow-pick").first().click();
|
||||
await p
|
||||
.getByRole("button", { name: "创作设置⌄", exact: true })
|
||||
.first()
|
||||
.click();
|
||||
ok(
|
||||
"切回工作流恢复独立参数",
|
||||
(await p
|
||||
.locator(".modal-panel input[type=number]")
|
||||
.first()
|
||||
.inputValue()) === "101",
|
||||
);
|
||||
await p.keyboard.press("Escape");
|
||||
await p.getByRole("tab", { name: "视频", exact: true }).click();
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
await p.locator(".rail-tools input[type=search]").fill("H3");
|
||||
await p.locator(".workflow-pick").first().click();
|
||||
await p.locator("#desk-prompt").fill("视频独立草稿");
|
||||
await p.getByRole("button", { name: "管理素材", exact: true }).click();
|
||||
const first = p
|
||||
.locator(".a-slot")
|
||||
.filter({ has: p.getByText("首帧", { exact: true }) }),
|
||||
last = p
|
||||
.locator(".a-slot")
|
||||
.filter({ has: p.getByText("尾帧", { exact: true }) });
|
||||
await first
|
||||
.locator("input[type=file]")
|
||||
.setInputFiles("calliope-web/static/refract/coast.jpg");
|
||||
await first.locator("img").waitFor();
|
||||
await last
|
||||
.locator("input[type=file]")
|
||||
.setInputFiles("calliope-web/static/refract/arch.jpg");
|
||||
await last.locator("img").waitFor();
|
||||
await p.getByRole("button", { name: "完成", exact: true }).click();
|
||||
await p.getByRole("tab", { name: "图片", exact: true }).click();
|
||||
await p.locator("#desk-negative").waitFor();
|
||||
ok(
|
||||
"图视频切换恢复图片草稿",
|
||||
(await p.locator("#desk-prompt").inputValue()) ===
|
||||
"这是当前输入,不随历史变化",
|
||||
);
|
||||
await p.getByRole("tab", { name: "视频", exact: true }).click();
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
ok(
|
||||
"图视频切换恢复视频工作流和上传素材",
|
||||
(await p.locator("#desk-prompt").inputValue()) === "视频独立草稿" &&
|
||||
(await p.locator(".media-token img").count()) === 2,
|
||||
);
|
||||
const v = await submit(p, f.h3, true);
|
||||
const player = p.locator(".print video");
|
||||
await player.waitFor();
|
||||
await wait(() => player.evaluate((e) => e.readyState >= 1));
|
||||
ok(
|
||||
"视频结果默认暂停",
|
||||
await player.evaluate((e) => e.paused && !e.autoplay),
|
||||
);
|
||||
await p.locator("#desk-prompt").focus();
|
||||
await p.keyboard.press("Space");
|
||||
ok("提示词中的空格不播放视频", await player.evaluate((e) => e.paused));
|
||||
await p.getByRole("button", { name: "播放此视频", exact: true }).click();
|
||||
await wait(() => player.evaluate((e) => !e.paused));
|
||||
await player.focus();
|
||||
await p.keyboard.press("Space");
|
||||
await wait(() => player.evaluate((e) => e.paused));
|
||||
ok("中央大播放键与聚焦空格可控", true);
|
||||
for (const [width, height] of [
|
||||
[1440, 900],
|
||||
[1366, 768],
|
||||
[1280, 720],
|
||||
]) {
|
||||
await p.setViewportSize({ width, height });
|
||||
await p.waitForTimeout(150);
|
||||
ok(
|
||||
width + " 视频完整且主按钮首屏",
|
||||
(await p.locator(".gen").evaluate((e) => {
|
||||
const r = e.getBoundingClientRect();
|
||||
return r.top >= 0 && r.bottom <= innerHeight && r.right <= innerWidth;
|
||||
})) &&
|
||||
(await player.evaluate(
|
||||
(e) => getComputedStyle(e).objectFit === "contain",
|
||||
)),
|
||||
);
|
||||
await shot(p, width + "-video");
|
||||
}
|
||||
// Cross-medium reuse from the complete history switches the editor kind explicitly.
|
||||
await p.getByRole("button", { name: "全部记录", exact: true }).click();
|
||||
await p
|
||||
.locator(".history-full .a-seg")
|
||||
.getByRole("button", { name: "全部", exact: true })
|
||||
.click();
|
||||
await p
|
||||
.locator('.history-full [data-job="' + j.id + '"]')
|
||||
.getByRole("button", { name: "复用", exact: true })
|
||||
.click();
|
||||
await p
|
||||
.getByRole("tab", { name: "图片", exact: true, selected: true })
|
||||
.waitFor();
|
||||
await p.locator("#desk-negative").waitFor();
|
||||
ok(
|
||||
"跨媒介历史复用切到所属类型并回填",
|
||||
(await p.locator("#desk-prompt").inputValue()) ===
|
||||
"一位女孩站在海岸,清晨柔光。",
|
||||
);
|
||||
await p.getByRole("tab", { name: "视频", exact: true }).click();
|
||||
await p.locator(".print video").waitFor();
|
||||
// Theme switches preserve the same editor node and state.
|
||||
await p.locator("#desk-prompt").evaluate((e) => (window.__editor = e));
|
||||
for (const [id, name] of [
|
||||
["night", "夜片场"],
|
||||
["day", "日光棚"],
|
||||
["grade", "调色间"],
|
||||
["bluehour", "蓝调时刻"],
|
||||
]) {
|
||||
await p.getByRole("button", { name: "主题", exact: true }).click();
|
||||
await p.locator(".choice").filter({ hasText: name }).click();
|
||||
await p.keyboard.press("Escape");
|
||||
ok(
|
||||
id + " 切换保持编辑和素材",
|
||||
(await p
|
||||
.locator("#desk-prompt")
|
||||
.evaluate((e) => e === window.__editor)) &&
|
||||
(await p.locator(".media-token img").count()) === 2,
|
||||
);
|
||||
await shot(p, "1280-theme-" + id);
|
||||
}
|
||||
|
||||
// Import minimal API fixtures with the same parser roles used by user workflows.
|
||||
const base = await api("/api/workflows/" + f.dual);
|
||||
const maskBody = {
|
||||
...base.workflow_json,
|
||||
5: {
|
||||
class_type: "LoadImage",
|
||||
inputs: { image: "" },
|
||||
_meta: { title: "(Input:image) 输入图" },
|
||||
},
|
||||
6: {
|
||||
class_type: "LoadImageMask",
|
||||
inputs: { image: "", channel: "red" },
|
||||
_meta: { title: "(Input:mask) 遮罩" },
|
||||
},
|
||||
};
|
||||
const maskWf = await api("/api/workflows", {
|
||||
name: "遮罩图生图测试",
|
||||
kind: "image",
|
||||
workflow_json: maskBody,
|
||||
});
|
||||
await p.goto(WEB + "/create?wf=" + maskWf.id);
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
await p.getByRole("button", { name: "管理素材", exact: true }).click();
|
||||
const imageSlots = p.locator(".a-slot");
|
||||
ok("图生图与遮罩独立槽位", (await imageSlots.count()) === 2);
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await imageSlots
|
||||
.nth(i)
|
||||
.locator("input[type=file]")
|
||||
.setInputFiles("calliope-web/static/refract/coast.jpg");
|
||||
await imageSlots.nth(i).locator("img").waitFor();
|
||||
}
|
||||
await p.getByRole("button", { name: "完成", exact: true }).click();
|
||||
const masked = await submit(p, maskWf.id);
|
||||
ok(
|
||||
"上传和遮罩经真实队列生成",
|
||||
!!masked.payload.input_values["5"] && !!masked.payload.input_values["6"],
|
||||
);
|
||||
const refBody = {
|
||||
1: {
|
||||
class_type: "CLIPTextEncode",
|
||||
inputs: { text: "多参考视频" },
|
||||
_meta: { title: "提示词" },
|
||||
},
|
||||
5: { class_type: "LoadImage", inputs: { image: "" } },
|
||||
7: {
|
||||
class_type: "MiniMaxH3ReferenceToVideo",
|
||||
inputs: { "ref_images.ref_image_0": ["5", 0] },
|
||||
},
|
||||
9: {
|
||||
class_type: "VHS_VideoCombine",
|
||||
inputs: { images: "x" },
|
||||
_meta: { title: "(Output:video) 视频输出" },
|
||||
},
|
||||
};
|
||||
const refWf = await api("/api/workflows", {
|
||||
name: "H3多参考测试",
|
||||
kind: "video",
|
||||
workflow_json: refBody,
|
||||
});
|
||||
await p.goto(WEB + "/create?kind=video&wf=" + refWf.id);
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
await p.getByRole("button", { name: "管理素材", exact: true }).click();
|
||||
await p
|
||||
.locator(".media-editor input[type=file]")
|
||||
.first()
|
||||
.setInputFiles([
|
||||
"calliope-web/static/refract/coast.jpg",
|
||||
"calliope-web/static/refract/arch.jpg",
|
||||
]);
|
||||
await wait(
|
||||
async () => (await p.locator(".media-editor img").count()) === 2,
|
||||
);
|
||||
await p.getByRole("button", { name: "完成", exact: true }).click();
|
||||
const refs = await submit(p, refWf.id, true);
|
||||
ok(
|
||||
"多参考素材真实上传并生成",
|
||||
Array.isArray(refs.payload.input_values["7@ref_images"]) &&
|
||||
refs.payload.input_values["7@ref_images"].length === 2,
|
||||
);
|
||||
await shot(p, "1280-multi-reference");
|
||||
// Preserve failure, retry and cancellation paths.
|
||||
await p.goto(WEB + "/create?wf=" + f.dual);
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
mode(false, { mode: "fail" });
|
||||
await p.locator(".gen").click();
|
||||
const failed = await wait(async () =>
|
||||
(await api("/api/playground/jobs")).find(
|
||||
(j) => j.workflow_id === f.dual && j.status === "failed",
|
||||
),
|
||||
);
|
||||
await p.getByRole("button", { name: "重试", exact: true }).waitFor();
|
||||
mode();
|
||||
await p.getByRole("button", { name: "重试", exact: true }).click();
|
||||
await wait(async () =>
|
||||
(await api("/api/playground/jobs")).some(
|
||||
(j) => j.id === failed.id && j.status === "done",
|
||||
),
|
||||
);
|
||||
ok("失败后重试取得真实结果", true);
|
||||
mode(false, { delay_sec: 10 });
|
||||
await p.locator(".gen").click();
|
||||
await p.getByRole("button", { name: "取消生成", exact: true }).waitFor();
|
||||
await p.getByRole("button", { name: "取消生成", exact: true }).click();
|
||||
await wait(async () =>
|
||||
(await api("/api/playground/jobs")).some(
|
||||
(j) =>
|
||||
j.workflow_id === f.dual &&
|
||||
j.status === "failed" &&
|
||||
j.error === "cancelled",
|
||||
),
|
||||
);
|
||||
ok("进行中生成可以取消", true);
|
||||
mode();
|
||||
// Pending rows are hidden by default and point to the actual mapping/testing row.
|
||||
const w = await api("/api/workflows/" + f.dual);
|
||||
const pending = await api("/api/workflows", {
|
||||
name: "待配置测试工具",
|
||||
kind: "image",
|
||||
workflow_json: w.workflow_json,
|
||||
});
|
||||
await api("/api/workflows/" + pending.id, { is_enabled: false }, "PATCH");
|
||||
await p.goto(WEB + "/create");
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
await p.locator(".rail-tools input[type=search]").fill("待配置测试");
|
||||
ok("默认仅显示可运行", (await p.locator(".workflow-pick").count()) === 0);
|
||||
await p.getByRole("checkbox", { name: "仅可运行" }).uncheck();
|
||||
await p
|
||||
.locator(".workflow-pick")
|
||||
.filter({ hasText: "待配置测试工具" })
|
||||
.click();
|
||||
await p.waitForURL("**/workflows?manage=1&wf=" + pending.id);
|
||||
ok(
|
||||
"待配置导向真实管理和测试入口",
|
||||
await p.locator(".wf-test-btn").first().isVisible(),
|
||||
);
|
||||
// Source project round-trip uses original API and explicit attachment.
|
||||
await api(
|
||||
"/api/workbench/projects/" + f.project + "/stages/assets",
|
||||
{
|
||||
content: {
|
||||
assets: [
|
||||
{
|
||||
id: "b1-source",
|
||||
kind: "character",
|
||||
name: "海边女孩",
|
||||
prompt: "源项目提示词",
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"PATCH",
|
||||
);
|
||||
const q = new URLSearchParams({
|
||||
kind: "image",
|
||||
wf: String(f.dual),
|
||||
from: "workbench",
|
||||
project: String(f.project),
|
||||
stage: "assets",
|
||||
source_kind: "asset",
|
||||
source_id: "b1-source",
|
||||
label: "海边女孩",
|
||||
prompt: "源项目提示词",
|
||||
});
|
||||
await p.goto(WEB + "/create?" + q);
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
const flow = await submit(p, f.dual);
|
||||
ok(
|
||||
"来源随真实生成请求发送",
|
||||
flow.payload?.workbench_context?.source_id === "b1-source" ||
|
||||
JSON.stringify(flow.payload).includes("b1-source"),
|
||||
);
|
||||
await p.getByRole("button", { name: "存入项目", exact: true }).click();
|
||||
await wait(async () =>
|
||||
JSON.stringify(
|
||||
await api("/api/workbench/projects/" + f.project),
|
||||
).includes(flow.output_paths[0]),
|
||||
);
|
||||
ok("结果显式回流来源项目", true);
|
||||
// Create a deterministic portrait fixture locally (no external image request).
|
||||
const fixture = await c.newPage();
|
||||
await fixture.setViewportSize({ width: 480, height: 720 });
|
||||
await fixture.setContent(
|
||||
'<body style="margin:0;background:linear-gradient(160deg,#213b42,#d9a86c);height:100vh;display:grid;place-items:center"><div style="width:220px;height:360px;background:#192a32;border-radius:50% 50% 10% 10%;box-shadow:0 0 80px #ecbd7b"></div></body>',
|
||||
);
|
||||
await fixture.screenshot({ path: out + "/portrait.png" });
|
||||
await fixture.close();
|
||||
ok("竖图测试素材为480×720", await fixtureDimensions(out + "/portrait.png"));
|
||||
// Portrait & landscape outputs in the real application.
|
||||
for (const file of ["coast.jpg", "portrait.png"]) {
|
||||
const img = path.resolve(
|
||||
file === "portrait.png"
|
||||
? out + "/portrait.png"
|
||||
: "calliope-web/static/refract/" + file,
|
||||
);
|
||||
mode(false, { image_file: img });
|
||||
const before = Math.max(
|
||||
...(await api("/api/playground/jobs")).map((j) => j.id),
|
||||
);
|
||||
await p.locator(".gen").click();
|
||||
const image = await wait(async () =>
|
||||
(await api("/api/playground/jobs")).find(
|
||||
(j) => j.id > before && j.status === "done",
|
||||
),
|
||||
);
|
||||
await p
|
||||
.locator(".result-title")
|
||||
.filter({ hasText: "第 " + image.id + " 次生成" })
|
||||
.waitFor();
|
||||
await p.locator(".print img").waitFor();
|
||||
for (const [width, height] of [
|
||||
[1440, 900],
|
||||
[1366, 768],
|
||||
[1280, 720],
|
||||
]) {
|
||||
await p.setViewportSize({ width, height });
|
||||
await p.waitForTimeout(120);
|
||||
ok(
|
||||
width + " " + file + " 媒体比例正确",
|
||||
await p.locator(".print img").evaluate((e) => {
|
||||
const r = e.getBoundingClientRect();
|
||||
return (
|
||||
Math.abs(r.width / r.height - e.naturalWidth / e.naturalHeight) <
|
||||
0.02 &&
|
||||
r.bottom <= innerHeight &&
|
||||
r.top >= 0
|
||||
);
|
||||
}),
|
||||
);
|
||||
await shot(p, width + "-" + file);
|
||||
}
|
||||
}
|
||||
await p.goto(
|
||||
WEB + "/playground?kind=video&wf=" + f.h3 + "&from=test#legacy",
|
||||
);
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
ok(
|
||||
"试验台兼容入口保留参数及片段",
|
||||
new URL(p.url()).pathname === "/create" &&
|
||||
new URL(p.url()).searchParams.get("from") === "test" &&
|
||||
new URL(p.url()).hash === "#legacy",
|
||||
);
|
||||
const blocked = await browser.newContext({
|
||||
viewport: { width: 1280, height: 720 },
|
||||
});
|
||||
await blocked.addInitScript(() => {
|
||||
Storage.prototype.getItem = function () {
|
||||
throw Error("denied");
|
||||
};
|
||||
Storage.prototype.setItem = function () {
|
||||
throw Error("denied");
|
||||
};
|
||||
});
|
||||
const bp = await blocked.newPage();
|
||||
await bp.goto(WEB + "/create?wf=" + f.dual);
|
||||
await bp.locator("#desk-prompt").fill("存储禁止也保留草稿");
|
||||
await bp.getByRole("tab", { name: "视频", exact: true }).click();
|
||||
await bp.locator("#desk-prompt").fill("视频草稿");
|
||||
await bp.getByRole("tab", { name: "图片", exact: true }).click();
|
||||
await bp.locator("#desk-negative").waitFor();
|
||||
ok(
|
||||
"浏览器存储被禁止时仍可切换并恢复草稿",
|
||||
(await bp.locator("#desk-prompt").inputValue()) === "存储禁止也保留草稿",
|
||||
);
|
||||
await blocked.close();
|
||||
ok("没有浏览器运行异常", errors.length === 0);
|
||||
fs.writeFileSync(
|
||||
out + "/results.json",
|
||||
JSON.stringify({ checks, errors }, null, 2),
|
||||
);
|
||||
} catch (e) {
|
||||
if (p) await shot(p, "failure").catch(() => {});
|
||||
throw e;
|
||||
} finally {
|
||||
if (saved) fs.writeFileSync("e2e/fake_mode.json", saved);
|
||||
else if (fs.existsSync("e2e/fake_mode.json"))
|
||||
fs.unlinkSync("e2e/fake_mode.json");
|
||||
await browser.close();
|
||||
}
|
||||
})().catch((e) => {
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
+605
@@ -0,0 +1,605 @@
|
||||
// HEL-740: same-viewport sample comparisons plus anchored-dialog/state evidence.
|
||||
const { chromium } = require(
|
||||
process.env.PLAYWRIGHT_MODULE_PATH || "playwright",
|
||||
);
|
||||
const fs = require("node:fs"),
|
||||
path = require("node:path"),
|
||||
assert = require("node:assert/strict");
|
||||
const out = "e2e/shots/hel740";
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
let currentPage;
|
||||
const checks = [],
|
||||
measurements = [],
|
||||
errors = [];
|
||||
const ok = (name, value) => {
|
||||
assert(value, name);
|
||||
checks.push(name);
|
||||
console.log("PASS", name);
|
||||
};
|
||||
const api = async (p) => (await fetch("http://127.0.0.1:8247" + p)).json();
|
||||
const pause = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
async function wait(fn) {
|
||||
for (let i = 0; i < 160; i++) {
|
||||
if (await fn()) return;
|
||||
await pause(100);
|
||||
}
|
||||
throw Error("timeout");
|
||||
}
|
||||
async function metrics(page, sample = false) {
|
||||
return page.evaluate((sample) => {
|
||||
const selectors = sample
|
||||
? {
|
||||
button: "#genBtn",
|
||||
dock: "#dock",
|
||||
positive: "#posTa",
|
||||
negative: "#negTa",
|
||||
segment: "#kindSeg",
|
||||
name: "#hdName",
|
||||
history: ".hist",
|
||||
actions: "#actbar",
|
||||
pop: "#paramPop",
|
||||
search: "#wfSearch",
|
||||
searchBox: "#wfSearch",
|
||||
filter: "#taskChips .chip",
|
||||
meta: ".wfb-meta",
|
||||
head: ".cc > .ph",
|
||||
file: "#hdFile",
|
||||
action: "#actbar > button",
|
||||
separator: "#actbar .sep",
|
||||
remove: "#actbar [data-act=del]",
|
||||
icon: "#genBtn svg",
|
||||
}
|
||||
: {
|
||||
button: ".dock .gen",
|
||||
dock: ".dock .composer",
|
||||
positive: "#desk-prompt",
|
||||
negative: "#desk-negative",
|
||||
segment: ".kind-tabs",
|
||||
name: ".model-selector",
|
||||
history: ".history-rail",
|
||||
actions: ".artboard-footer",
|
||||
pop: ".anchored .modal-panel",
|
||||
search: ".rail-search input",
|
||||
searchBox: ".rail-search",
|
||||
filter: ".filters button",
|
||||
meta: ".list-meta",
|
||||
head: ".studio-heading",
|
||||
file: ".workflow-file",
|
||||
action: ".artboard-footer > button",
|
||||
separator: ".action-separator",
|
||||
remove: ".delete-output",
|
||||
icon: ".dock .gen svg",
|
||||
};
|
||||
return Object.fromEntries(
|
||||
Object.entries(selectors).map(([key, sel]) => {
|
||||
let e = document.querySelector(sel);
|
||||
if (!e) return [key, null];
|
||||
if (sample && key === "searchBox") e = e.parentElement;
|
||||
let r = e.getBoundingClientRect(),
|
||||
s = getComputedStyle(e);
|
||||
return [
|
||||
key,
|
||||
{
|
||||
x: r.x,
|
||||
y: r.y,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
bottom: r.bottom,
|
||||
radius: s.borderRadius,
|
||||
font: s.fontSize,
|
||||
weight: s.fontWeight,
|
||||
background: s.backgroundImage,
|
||||
border: s.borderTopWidth,
|
||||
shadow: s.boxShadow,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
}, sample);
|
||||
}
|
||||
async function shot(page, name) {
|
||||
await page.screenshot({ path: out + "/" + name + ".png" });
|
||||
}
|
||||
(async () => {
|
||||
const b = await chromium.launch({
|
||||
executablePath: process.env.CHROMIUM_EXECUTABLE_PATH,
|
||||
args: ["--no-sandbox", "--disable-dev-shm-usage"],
|
||||
});
|
||||
const saved = fs.existsSync("e2e/fake_mode.json")
|
||||
? fs.readFileSync("e2e/fake_mode.json")
|
||||
: null;
|
||||
try {
|
||||
const c = await b.newContext({
|
||||
viewport: { width: 1440, height: 900 },
|
||||
recordVideo: {
|
||||
dir: out + "/video",
|
||||
size: { width: 1440, height: 900 },
|
||||
},
|
||||
}),
|
||||
p = await c.newPage(),
|
||||
s = await c.newPage();
|
||||
currentPage = p;
|
||||
p.on("pageerror", (e) => errors.push(e.message));
|
||||
p.setDefaultTimeout(12000);
|
||||
await s.goto(
|
||||
"file://" +
|
||||
path.resolve(
|
||||
process.env.HEL740_SAMPLE ||
|
||||
"../references/夜片场-多主题交互样本.html",
|
||||
) +
|
||||
"?still=1&fill=1#create/image",
|
||||
);
|
||||
await s.locator("#posTa").waitFor();
|
||||
const data = await s.evaluate(async () => {
|
||||
const img = new Image();
|
||||
img.src = st.hist.image[0].img;
|
||||
await img.decode();
|
||||
const cv = document.createElement("canvas");
|
||||
cv.width = img.naturalWidth;
|
||||
cv.height = img.naturalHeight;
|
||||
cv.getContext("2d").drawImage(img, 0, 0);
|
||||
return {
|
||||
img: cv.toDataURL("image/png"),
|
||||
pos: $("#posTa").value,
|
||||
neg: $("#negTa").value,
|
||||
};
|
||||
});
|
||||
fs.writeFileSync(
|
||||
out + "/sample-media.png",
|
||||
Buffer.from(data.img.split(",")[1], "base64"),
|
||||
);
|
||||
const workflows = await api("/api/workflows"),
|
||||
mask = workflows.find((w) => w.name === "遮罩图生图测试");
|
||||
assert(mask);
|
||||
await p.goto("http://127.0.0.1:5173/create?wf=" + mask.id);
|
||||
await p.locator("#desk-negative").waitFor();
|
||||
const jobs = await api("/api/playground/jobs"),
|
||||
job = jobs.find((j) => j.workflow_id === mask.id && j.status === "done");
|
||||
assert(job);
|
||||
await p
|
||||
.locator(".history-take")
|
||||
.filter({ hasText: "#" + job.id })
|
||||
.click();
|
||||
await p.getByRole("button", { name: "复用参数", exact: true }).click();
|
||||
await p.locator("#desk-prompt").fill(data.pos);
|
||||
await p.locator("#desk-negative").fill(data.neg);
|
||||
fs.writeFileSync(
|
||||
"e2e/fake_mode.json",
|
||||
JSON.stringify({
|
||||
mode: "ok",
|
||||
unique_filename: true,
|
||||
image_file: path.resolve(out + "/sample-media.png"),
|
||||
}),
|
||||
);
|
||||
const before = Math.max(...jobs.map((j) => j.id));
|
||||
await p.locator(".gen").click();
|
||||
await wait(async () =>
|
||||
(await api("/api/playground/jobs")).some(
|
||||
(j) => j.id > before && j.status === "done",
|
||||
),
|
||||
);
|
||||
await wait(() =>
|
||||
p.locator(".print img").evaluate((e) => e.complete && e.naturalWidth > 0),
|
||||
);
|
||||
await pause(1000);
|
||||
for (const [width, height] of [
|
||||
[1440, 900],
|
||||
[1366, 768],
|
||||
[1280, 720],
|
||||
]) {
|
||||
await p.setViewportSize({ width, height });
|
||||
await s.setViewportSize({ width, height });
|
||||
for (const theme of ["night", "day", "grade", "bluehour"]) {
|
||||
await p.getByRole("button", { name: "主题", exact: true }).click();
|
||||
await p
|
||||
.locator(".choice")
|
||||
.filter({
|
||||
hasText: {
|
||||
night: "夜片场",
|
||||
day: "日光棚",
|
||||
grade: "调色间",
|
||||
bluehour: "蓝调时刻",
|
||||
}[theme],
|
||||
})
|
||||
.click();
|
||||
await p.keyboard.press("Escape");
|
||||
await s.evaluate((t) => {
|
||||
document.documentElement.dataset.theme = t;
|
||||
renderCreate();
|
||||
}, theme);
|
||||
await pause(650);
|
||||
const actual = await metrics(p),
|
||||
sample = await metrics(s, true);
|
||||
measurements.push({ width, height, theme, actual, sample });
|
||||
ok(
|
||||
`${width}/${theme} 正负提示词宽度与样本误差不超过1px`,
|
||||
Math.abs(actual.positive.width - sample.positive.width) <= 1 &&
|
||||
Math.abs(actual.negative.width - sample.negative.width) <= 1,
|
||||
);
|
||||
ok(
|
||||
`${width}/${theme} 按钮38高、提示词60高13px、操作条40高、分段36高`,
|
||||
actual.button.height === 38 &&
|
||||
actual.positive.height === 60 &&
|
||||
actual.positive.font === "13px" &&
|
||||
actual.actions.height === 40 &&
|
||||
actual.segment.height === 36,
|
||||
);
|
||||
ok(
|
||||
`${width}/${theme} 首屏按钮与作品完整`,
|
||||
actual.button.bottom <= height &&
|
||||
(await p.locator(".print img").evaluate((e) => {
|
||||
let r = e.getBoundingClientRect();
|
||||
return (
|
||||
r.top >= 48 &&
|
||||
r.bottom < innerHeight &&
|
||||
getComputedStyle(e).objectFit === "contain"
|
||||
);
|
||||
})),
|
||||
);
|
||||
ok(
|
||||
`${width}/${theme} R1 搜索与筛选chips几何`,
|
||||
actual.searchBox.height === sample.searchBox.height &&
|
||||
actual.searchBox.radius === sample.searchBox.radius &&
|
||||
actual.search.font === sample.search.font &&
|
||||
actual.filter.height === sample.filter.height &&
|
||||
actual.filter.font === sample.filter.font &&
|
||||
actual.filter.radius === sample.filter.radius,
|
||||
);
|
||||
ok(
|
||||
`${width}/${theme} R2 头行与裸文本文件名`,
|
||||
actual.head.height === sample.head.height &&
|
||||
Math.abs(actual.name.height - sample.name.height) < 0.1 &&
|
||||
actual.name.font === sample.name.font &&
|
||||
actual.name.weight === sample.name.weight &&
|
||||
actual.file.font === sample.file.font &&
|
||||
(await p.locator(".studio-heading button").count()) === 0,
|
||||
);
|
||||
ok(
|
||||
`${width}/${theme} R3 删除钮与分隔线`,
|
||||
actual.action.radius === sample.action.radius &&
|
||||
actual.remove.width === 26 &&
|
||||
actual.remove.height === 26 &&
|
||||
actual.separator.width === 1 &&
|
||||
(await p.locator(".action-separator").count()) === 2,
|
||||
);
|
||||
ok(
|
||||
`${width}/${theme} R4 14px图标及主题按钮宽度`,
|
||||
actual.icon.width === 14 &&
|
||||
Math.abs(actual.button.width - sample.button.width) <= 0.5,
|
||||
);
|
||||
ok(
|
||||
`${width}/${theme} meta真实未知且无横向溢出`,
|
||||
(await p
|
||||
.locator(".list-meta")
|
||||
.innerText()
|
||||
.then((t) => t.includes("仓库文件数未知"))) &&
|
||||
(await p.evaluate(
|
||||
() => document.documentElement.scrollWidth <= innerWidth,
|
||||
)),
|
||||
);
|
||||
await shot(s, `${width}-${theme}-sample`);
|
||||
await shot(p, `${width}-${theme}-actual`);
|
||||
const actions = p.locator(
|
||||
".artboard-footer > button,.artboard-footer > a",
|
||||
);
|
||||
ok(`${width}/${theme} 八入口显式存在`, (await actions.count()) === 8);
|
||||
}
|
||||
await p
|
||||
.getByRole("button", { name: "创作设置⌄", exact: true })
|
||||
.first()
|
||||
.click();
|
||||
await pause(250);
|
||||
const popup = await metrics(p);
|
||||
ok(
|
||||
`${width} 参数锚定底座上方且不遮主按钮`,
|
||||
popup.pop.bottom <= popup.dock.y - 7 &&
|
||||
popup.pop.y >= 48 &&
|
||||
popup.pop.width === 620,
|
||||
);
|
||||
await s.evaluate(() => openPop());
|
||||
await pause(250);
|
||||
measurements.push({
|
||||
width,
|
||||
height,
|
||||
state: "params",
|
||||
actual: await metrics(p),
|
||||
sample: await metrics(s, true),
|
||||
});
|
||||
await shot(p, `${width}-params-actual`);
|
||||
await shot(s, `${width}-params-sample`);
|
||||
// Focus cycle and draft-only reset.
|
||||
await p.locator(".anchored input[type=number]").first().fill("333");
|
||||
await p
|
||||
.getByRole("button", { name: "恢复工作流默认", exact: true })
|
||||
.click();
|
||||
ok(
|
||||
`${width} 恢复默认只改弹层草稿`,
|
||||
(await p
|
||||
.locator(".anchored input[type=number]")
|
||||
.first()
|
||||
.inputValue()) === "42",
|
||||
);
|
||||
await p.keyboard.press("Escape");
|
||||
ok(
|
||||
`${width} Escape归还焦点`,
|
||||
await p
|
||||
.getByRole("button", { name: "创作设置⌄", exact: true })
|
||||
.first()
|
||||
.evaluate((e) => e === document.activeElement),
|
||||
);
|
||||
await s.evaluate(() => $("#paramPop").classList.remove("on"));
|
||||
}
|
||||
await p.setViewportSize({ width: 1440, height: 900 });
|
||||
await p.getByRole("button", { name: "收起历史栏", exact: true }).click();
|
||||
ok(
|
||||
"历史收起列宽归零",
|
||||
(await p
|
||||
.locator(".history-rail")
|
||||
.evaluate((e) => e.getBoundingClientRect().width)) === 0,
|
||||
);
|
||||
await shot(p, "history-collapsed");
|
||||
await p.getByRole("button", { name: "展开历史栏", exact: true }).click();
|
||||
await p
|
||||
.locator(".history-take")
|
||||
.filter({ hasText: "#" + job.id })
|
||||
.click();
|
||||
await shot(p, "history-banner");
|
||||
ok(
|
||||
"历史胶囊999圆角",
|
||||
(await p
|
||||
.locator(".history-banner")
|
||||
.evaluate((e) => getComputedStyle(e).borderRadius)) === "999px",
|
||||
);
|
||||
await p.getByRole("button", { name: "返回当前创作", exact: true }).click();
|
||||
await p
|
||||
.getByRole("button", { name: "创作设置⌄", exact: true })
|
||||
.first()
|
||||
.click();
|
||||
const dialog = p.locator(".anchored .modal-panel");
|
||||
await dialog.locator("button").last().focus();
|
||||
await p.keyboard.press("Tab");
|
||||
ok(
|
||||
"参数Tab焦点循环",
|
||||
await dialog.evaluate((e) => e.contains(document.activeElement)),
|
||||
);
|
||||
await p.locator(".anchored input[type=number]").first().fill("234");
|
||||
await p.getByRole("button", { name: "取消", exact: true }).click();
|
||||
await p
|
||||
.getByRole("button", { name: "创作设置⌄", exact: true })
|
||||
.first()
|
||||
.click();
|
||||
ok(
|
||||
"参数取消丢弃修改",
|
||||
(await p.locator(".anchored input[type=number]").first().inputValue()) !==
|
||||
"234",
|
||||
);
|
||||
await p.locator(".anchored input[type=number]").first().fill("235");
|
||||
await p.getByRole("button", { name: "保存设置", exact: true }).click();
|
||||
await p
|
||||
.getByRole("button", { name: "创作设置⌄", exact: true })
|
||||
.first()
|
||||
.click();
|
||||
ok(
|
||||
"参数保存草稿持久化",
|
||||
(await p.locator(".anchored input[type=number]").first().inputValue()) ===
|
||||
"235",
|
||||
);
|
||||
await p
|
||||
.getByRole("button", { name: "恢复工作流默认", exact: true })
|
||||
.click();
|
||||
await p.getByRole("button", { name: "保存设置", exact: true }).click();
|
||||
await p
|
||||
.getByRole("button", { name: "创作设置⌄", exact: true })
|
||||
.first()
|
||||
.click();
|
||||
ok(
|
||||
"恢复默认可保存",
|
||||
(await p.locator(".anchored input[type=number]").first().inputValue()) ===
|
||||
"42",
|
||||
);
|
||||
await p.keyboard.press("Escape");
|
||||
// Actual existing disabled abilities have explanatory titles; no fake success.
|
||||
ok(
|
||||
"未具备送视频能力如实禁用",
|
||||
await p
|
||||
.getByRole("button", { name: "送去生视频", exact: true })
|
||||
.isDisabled(),
|
||||
);
|
||||
ok(
|
||||
"无来源时通用存入项目禁用",
|
||||
await p
|
||||
.getByRole("button", { name: "存入项目", exact: true })
|
||||
.isDisabled(),
|
||||
);
|
||||
await p.getByRole("button", { name: "放大查看", exact: true }).click();
|
||||
await shot(p, "zoom");
|
||||
await p.keyboard.press("Escape");
|
||||
await p
|
||||
.getByRole("button", { name: "作品详情与操作", exact: true })
|
||||
.click();
|
||||
await shot(p, "more-actions");
|
||||
await p.keyboard.press("Escape");
|
||||
// HEL-741: real filtering, retained entries and deletion of isolated data.
|
||||
const search = p.getByRole("searchbox", { name: "搜索工作流" });
|
||||
await search.fill("遮罩图生图测试");
|
||||
ok(
|
||||
"搜索更新真实筛选数",
|
||||
await p
|
||||
.locator(".list-meta")
|
||||
.innerText()
|
||||
.then((t) => t.startsWith("1 个")),
|
||||
);
|
||||
const usageGroup = p.getByRole("group", { name: "筛选工作流用途" });
|
||||
await usageGroup
|
||||
.getByRole("button", { name: "文生图", exact: true })
|
||||
.click();
|
||||
ok(
|
||||
"用途chips与搜索组合筛选",
|
||||
(await p.locator(".workflow-pick").count()) === 0,
|
||||
);
|
||||
await usageGroup.getByRole("button", { name: "全部", exact: true }).click();
|
||||
if (
|
||||
(await p
|
||||
.locator(".row-actions button")
|
||||
.last()
|
||||
.getAttribute("aria-pressed")) !== "true"
|
||||
)
|
||||
await p.locator(".row-actions button").last().click();
|
||||
await usageGroup
|
||||
.getByRole("button", { name: "☆ 收藏", exact: true })
|
||||
.click();
|
||||
ok(
|
||||
"收藏chip保留真实收藏筛选",
|
||||
(await p.locator(".workflow-pick").count()) === 1,
|
||||
);
|
||||
await shot(p, "filters-favorite");
|
||||
await usageGroup
|
||||
.getByRole("button", { name: "☆ 收藏", exact: true })
|
||||
.click();
|
||||
await search.fill("");
|
||||
await p.locator("[data-workflow-details]").click();
|
||||
await p.getByRole("dialog").waitFor();
|
||||
await shot(p, "workflow-details");
|
||||
await p.keyboard.press("Escape");
|
||||
ok(
|
||||
"说明入口关闭归还焦点",
|
||||
await p
|
||||
.locator("[data-workflow-details]")
|
||||
.evaluate((e) => e === document.activeElement),
|
||||
);
|
||||
await p.getByRole("button", { name: "创作设置⌄", exact: true }).click();
|
||||
await p.getByRole("button", { name: "专注创作 ⤢", exact: true }).click();
|
||||
await p.keyboard.press("Escape");
|
||||
ok("参数层保留专注功能", !(await p.locator(".workflow-rail").isVisible()));
|
||||
await shot(p, "focus-mode");
|
||||
await p.getByRole("button", { name: "创作设置⌄", exact: true }).click();
|
||||
await p.getByRole("button", { name: "退出专注 ⤢", exact: true }).click();
|
||||
await p.keyboard.press("Escape");
|
||||
const deleteTarget = (await api("/api/playground/jobs")).find(
|
||||
(j) => j.workflow_id === mask.id && j.status === "done",
|
||||
);
|
||||
assert(deleteTarget);
|
||||
await p.locator(".delete-output").click();
|
||||
ok(
|
||||
"删除图标先确认且不发DELETE",
|
||||
(await api("/api/playground/jobs")).some((j) => j.id === deleteTarget.id),
|
||||
);
|
||||
await shot(p, "delete-confirm");
|
||||
await p.getByRole("button", { name: "取消", exact: true }).click();
|
||||
ok(
|
||||
"删除取消保留记录并归还焦点",
|
||||
(await api("/api/playground/jobs")).some(
|
||||
(j) => j.id === deleteTarget.id,
|
||||
) &&
|
||||
(await p
|
||||
.locator(".delete-output")
|
||||
.evaluate((e) => e === document.activeElement)),
|
||||
);
|
||||
await p.locator(".delete-output").click();
|
||||
await p.getByRole("button", { name: "确认移除", exact: true }).click();
|
||||
await p.getByRole("button", { name: "撤销", exact: true }).click();
|
||||
ok(
|
||||
"确认后沿用撤销窗口",
|
||||
(await api("/api/playground/jobs")).some((j) => j.id === deleteTarget.id),
|
||||
);
|
||||
await shot(p, "delete-undone");
|
||||
await p.locator(".delete-output").click();
|
||||
await p.getByRole("button", { name: "确认移除", exact: true }).click();
|
||||
await wait(
|
||||
async () =>
|
||||
!(await api("/api/playground/jobs")).some(
|
||||
(j) => j.id === deleteTarget.id,
|
||||
),
|
||||
);
|
||||
ok(
|
||||
"隔离数据确认后真实删除",
|
||||
!(await api("/api/playground/jobs")).some(
|
||||
(j) => j.id === deleteTarget.id,
|
||||
),
|
||||
);
|
||||
// Video state evidence across the same three viewports.
|
||||
await p.getByRole("tab", { name: "视频", exact: true }).click();
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
const videos = (await api("/api/playground/jobs")).filter(
|
||||
(j) => j.kind === "video" && j.status === "done",
|
||||
);
|
||||
const vj = videos.find((j) => j.workflow_id === 4);
|
||||
assert(vj);
|
||||
await p
|
||||
.locator(".history-take")
|
||||
.filter({ hasText: "#" + vj.id })
|
||||
.click();
|
||||
await p.getByRole("button", { name: "复用参数", exact: true }).click();
|
||||
await p.locator(".print video").waitFor();
|
||||
await s.evaluate(() => go("create", { kind: "video" }));
|
||||
for (const [width, height] of [
|
||||
[1440, 900],
|
||||
[1366, 768],
|
||||
[1280, 720],
|
||||
]) {
|
||||
await p.setViewportSize({ width, height });
|
||||
await s.setViewportSize({ width, height });
|
||||
await pause(350);
|
||||
await shot(p, `${width}-video-actual`);
|
||||
await shot(s, `${width}-video-sample`);
|
||||
ok(
|
||||
`${width} 视频默认暂停且播放键64`,
|
||||
(await p.locator(".print video").evaluate((e) => e.paused)) &&
|
||||
(await p
|
||||
.locator(".big-play")
|
||||
.evaluate((e) => e.getBoundingClientRect().width === 64)),
|
||||
);
|
||||
}
|
||||
await p.getByRole("button", { name: "播放此视频", exact: true }).click();
|
||||
await pause(500);
|
||||
await shot(p, "video-playing");
|
||||
await p.locator(".print video").focus();
|
||||
await p.keyboard.press("Space");
|
||||
await shot(p, "video-paused");
|
||||
await p.getByRole("tab", { name: "图片", exact: true }).click();
|
||||
await p.locator(".workflow-pick").first().hover();
|
||||
await shot(p, "workflow-hover");
|
||||
await p.locator(".gen").hover();
|
||||
await shot(p, "generate-hover");
|
||||
fs.writeFileSync(
|
||||
"e2e/fake_mode.json",
|
||||
JSON.stringify({
|
||||
mode: "ok",
|
||||
delay_sec: 10,
|
||||
unique_filename: true,
|
||||
image_file: path.resolve(out + "/sample-media.png"),
|
||||
}),
|
||||
);
|
||||
await p.locator(".gen").click();
|
||||
await p.getByRole("button", { name: "取消生成", exact: true }).waitFor();
|
||||
await shot(p, "generating");
|
||||
await p.getByRole("button", { name: "取消生成", exact: true }).click();
|
||||
ok("无浏览器异常", errors.length === 0);
|
||||
fs.writeFileSync(
|
||||
out + "/measurements.json",
|
||||
JSON.stringify(measurements, null, 2),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
out + "/results.json",
|
||||
JSON.stringify({ checks, errors }, null, 2),
|
||||
);
|
||||
await c.close();
|
||||
fs.copyFileSync(await p.video().path(), out + "/actual-interactions.webm");
|
||||
fs.copyFileSync(await s.video().path(), out + "/sample-interactions.webm");
|
||||
} catch (e) {
|
||||
fs.writeFileSync(
|
||||
out + "/measurements.json",
|
||||
JSON.stringify(measurements, null, 2),
|
||||
);
|
||||
if (currentPage) await shot(currentPage, "failure").catch(() => {});
|
||||
throw e;
|
||||
} finally {
|
||||
if (saved) fs.writeFileSync("e2e/fake_mode.json", saved);
|
||||
else if (fs.existsSync("e2e/fake_mode.json"))
|
||||
fs.unlinkSync("e2e/fake_mode.json");
|
||||
await b.close();
|
||||
}
|
||||
})().catch((e) => {
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
// HEL-742 independent verification: pure text-to-image / text-to-video
|
||||
// workflows expose no pad-slot UI at all (no 管理素材 entry, no .a-slot),
|
||||
// no stale slot UI after mode/workflow switches; positive control on the
|
||||
// mask workflow (2 slots). Run inside the isolated stack.
|
||||
const { chromium } = require(
|
||||
process.env.PLAYWRIGHT_MODULE_PATH || "playwright",
|
||||
);
|
||||
const fs = require("node:fs");
|
||||
const out = "e2e/shots/hel742";
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const checks = [];
|
||||
const errors = [];
|
||||
const ok = (name, value) => {
|
||||
if (!value) throw new Error("FAIL " + name);
|
||||
checks.push(name);
|
||||
console.log("PASS", name);
|
||||
};
|
||||
const api = async (p, body) =>
|
||||
body
|
||||
? (
|
||||
await fetch("http://127.0.0.1:8247" + p, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
).json()
|
||||
: (await fetch("http://127.0.0.1:8247" + p)).json();
|
||||
const pause = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
(async () => {
|
||||
const b = await chromium.launch({
|
||||
executablePath: process.env.CHROMIUM_EXECUTABLE_PATH,
|
||||
args: ["--no-sandbox", "--disable-dev-shm-usage"],
|
||||
});
|
||||
try {
|
||||
const c = await b.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const p = await c.newPage();
|
||||
p.on("pageerror", (e) => errors.push(e.message));
|
||||
p.setDefaultTimeout(15000);
|
||||
const wfs = await api("/api/workflows");
|
||||
const t2i = wfs.find((w) => w.name === "文生图·回归");
|
||||
const t2v = wfs.find((w) => w.name === "文生视频·回归");
|
||||
let mask = wfs.find((w) => w.name === "遮罩图生图测试");
|
||||
ok("隔离夹具含文生图/文生视频工作流", t2i && t2v);
|
||||
if (!mask) {
|
||||
const base = await api("/api/workflows/" + t2i.id);
|
||||
const maskBody = {
|
||||
...base.workflow_json,
|
||||
5: {
|
||||
class_type: "LoadImage",
|
||||
inputs: { image: "" },
|
||||
_meta: { title: "(Input:image) 输入图" },
|
||||
},
|
||||
6: {
|
||||
class_type: "LoadImageMask",
|
||||
inputs: { image: "", channel: "red" },
|
||||
_meta: { title: "(Input:mask) 遮罩" },
|
||||
},
|
||||
};
|
||||
mask = await api("/api/workflows", {
|
||||
name: "遮罩图生图测试",
|
||||
kind: "image",
|
||||
workflow_json: maskBody,
|
||||
});
|
||||
}
|
||||
|
||||
const noSlotUi = async () =>
|
||||
(await p.locator(".a-slot").count()) === 0 &&
|
||||
(await p
|
||||
.getByRole("button", { name: "管理素材", exact: true })
|
||||
.count()) === 0;
|
||||
|
||||
await p.goto("http://127.0.0.1:5173/create?wf=" + t2i.id);
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
await pause(400);
|
||||
await p.screenshot({ path: out + "/t2i-no-slot.png" });
|
||||
ok("文生图无图片输入:无垫图槽、无素材入口", await noSlotUi());
|
||||
|
||||
await p.getByRole("tab", { name: "视频", exact: true }).click();
|
||||
await p.locator(".rail-tools input[type=search]").fill("文生视频");
|
||||
await p.locator(".workflow-pick").first().click();
|
||||
await pause(600);
|
||||
await p.screenshot({ path: out + "/t2v-no-slot.png" });
|
||||
ok("文生视频无图片输入:无首尾帧槽、无素材入口", await noSlotUi());
|
||||
|
||||
await p.getByRole("tab", { name: "图片", exact: true }).click();
|
||||
await p.locator(".rail-tools input[type=search]").fill("文生图");
|
||||
await p.locator(".workflow-pick").first().click();
|
||||
await pause(600);
|
||||
ok("切回图片后无残留垫图槽", await noSlotUi());
|
||||
|
||||
await p.locator(".rail-tools input[type=search]").fill("遮罩");
|
||||
await p.locator(".workflow-pick").first().click();
|
||||
await pause(600);
|
||||
await p.getByRole("button", { name: "管理素材", exact: true }).click();
|
||||
await pause(400);
|
||||
ok(
|
||||
"遮罩工作流仍有两个图片槽(对照)",
|
||||
(await p.locator(".a-slot").count()) === 2,
|
||||
);
|
||||
await p.getByRole("button", { name: "完成", exact: true }).click();
|
||||
|
||||
await p.locator(".rail-tools input[type=search]").fill("文生图");
|
||||
await p.locator(".workflow-pick").first().click();
|
||||
await pause(600);
|
||||
ok("从遮罩切回文生图后槽位入口消失", await noSlotUi());
|
||||
ok("无浏览器异常", errors.length === 0);
|
||||
fs.writeFileSync(
|
||||
out + "/results.json",
|
||||
JSON.stringify({ checks, errors }, null, 2),
|
||||
);
|
||||
} finally {
|
||||
await b.close();
|
||||
}
|
||||
})();
|
||||
+351
@@ -0,0 +1,351 @@
|
||||
// HEL-743: sample geometry, themes, breakpoint and search keyboard regression.
|
||||
// Run using e2e/run-hel729.py with HEL729_TESTS=e2e/hel743.cjs.
|
||||
const { chromium } = require(
|
||||
process.env.PLAYWRIGHT_MODULE_PATH || "playwright",
|
||||
);
|
||||
const fs = require("node:fs"),
|
||||
path = require("node:path"),
|
||||
assert = require("node:assert/strict");
|
||||
const out = "e2e/shots/hel743";
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const checks = [],
|
||||
measurements = [],
|
||||
errors = [];
|
||||
function ok(name, v) {
|
||||
assert(v, name);
|
||||
checks.push(name);
|
||||
console.log("PASS", name);
|
||||
}
|
||||
const pause = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
async function metrics(p, sample = false) {
|
||||
return p.evaluate((sample) => {
|
||||
const sel = sample
|
||||
? {
|
||||
head: ".cc > .ph",
|
||||
name: "#hdName",
|
||||
file: "#hdFile",
|
||||
search: "#wfSearch",
|
||||
box: "#wfSearch",
|
||||
key: ".wfb-top kbd",
|
||||
}
|
||||
: {
|
||||
head: ".studio-heading",
|
||||
name: ".model-selector",
|
||||
file: ".workflow-file",
|
||||
search: ".rail-search input",
|
||||
box: ".rail-search",
|
||||
key: ".rail-search kbd",
|
||||
};
|
||||
const entries = Object.entries(sel).map(([k, sel]) => {
|
||||
let e = document.querySelector(sel);
|
||||
if (sample && k === "box") e = e.parentElement;
|
||||
if (sample && k === "key")
|
||||
e = document
|
||||
.querySelector("#wfSearch")
|
||||
.parentElement.querySelector("kbd");
|
||||
let r = e.getBoundingClientRect(),
|
||||
s = getComputedStyle(e);
|
||||
return [
|
||||
k,
|
||||
{
|
||||
x: r.x,
|
||||
y: r.y,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
font: s.font,
|
||||
fontSize: s.fontSize,
|
||||
fontWeight: s.fontWeight,
|
||||
lineHeight: s.lineHeight,
|
||||
padding: s.padding,
|
||||
radius: s.borderRadius,
|
||||
border: s.border,
|
||||
bottomBorder: s.borderBottom,
|
||||
background: s.backgroundColor,
|
||||
color: s.color,
|
||||
blur: s.backdropFilter,
|
||||
text: e.textContent,
|
||||
},
|
||||
];
|
||||
});
|
||||
return {
|
||||
...Object.fromEntries(entries),
|
||||
overflow: document.documentElement.scrollWidth > innerWidth,
|
||||
};
|
||||
}, sample);
|
||||
}
|
||||
(async () => {
|
||||
const b = await chromium.launch({
|
||||
executablePath: process.env.CHROMIUM_EXECUTABLE_PATH,
|
||||
args: ["--no-sandbox", "--disable-dev-shm-usage"],
|
||||
});
|
||||
try {
|
||||
const c = await b.newContext({ viewport: { width: 1440, height: 900 } }),
|
||||
p = await c.newPage(),
|
||||
s = await c.newPage();
|
||||
p.on("pageerror", (e) => errors.push(e.message));
|
||||
p.setDefaultTimeout(10000);
|
||||
const wfs = await (
|
||||
await fetch("http://127.0.0.1:8247/api/workflows")
|
||||
).json(),
|
||||
wf = wfs.find((w) => w.name === "遮罩图生图测试");
|
||||
assert(wf);
|
||||
// The fixture file and metadata live only under the isolated test data directory.
|
||||
const settings = await (
|
||||
await fetch("http://127.0.0.1:8247/api/settings")
|
||||
).json();
|
||||
assert.equal(
|
||||
path.resolve(settings.data_dir),
|
||||
path.resolve("e2e/shots/reference/data"),
|
||||
);
|
||||
const detail = await (
|
||||
await fetch("http://127.0.0.1:8247/api/workflows/" + wf.id)
|
||||
).json();
|
||||
const source = path.join(settings.data_dir, "hel743-mask-workflow.json");
|
||||
fs.writeFileSync(source, JSON.stringify(detail.workflow_json, null, 2));
|
||||
require("node:child_process").execFileSync(
|
||||
"calliope-backend/.venv/bin/python",
|
||||
[
|
||||
"-c",
|
||||
'import sqlite3,sys; c=sqlite3.connect(sys.argv[1]); c.execute("UPDATE workflows SET source_path=? WHERE id=?",(sys.argv[2],int(sys.argv[3]))); c.commit(); c.close()',
|
||||
path.join(settings.data_dir, "calliope.db"),
|
||||
source,
|
||||
String(wf.id),
|
||||
],
|
||||
);
|
||||
await p.goto("http://127.0.0.1:5173/create?wf=" + wf.id);
|
||||
await p.locator("#desk-prompt").waitFor();
|
||||
await s.goto(
|
||||
"file://" +
|
||||
path.resolve(process.env.HEL740_SAMPLE) +
|
||||
"?still=1&fill=1#create/image",
|
||||
);
|
||||
await s.locator("#wfSearch").waitFor();
|
||||
for (const [width, height] of [
|
||||
[1440, 900],
|
||||
[1366, 768],
|
||||
[1280, 720],
|
||||
]) {
|
||||
await p.setViewportSize({ width, height });
|
||||
await s.setViewportSize({ width, height });
|
||||
for (const theme of ["night", "day", "grade", "bluehour"]) {
|
||||
await p.getByRole("button", { name: "主题", exact: true }).click();
|
||||
await p
|
||||
.locator(".choice")
|
||||
.filter({
|
||||
hasText: {
|
||||
night: "夜片场",
|
||||
day: "日光棚",
|
||||
grade: "调色间",
|
||||
bluehour: "蓝调时刻",
|
||||
}[theme],
|
||||
})
|
||||
.click();
|
||||
await p.keyboard.press("Escape");
|
||||
await s.evaluate((t) => {
|
||||
document.documentElement.dataset.theme = t;
|
||||
renderCreate();
|
||||
}, theme);
|
||||
await pause(450);
|
||||
const actual = await metrics(p),
|
||||
sample = await metrics(s, true);
|
||||
measurements.push({ width, height, theme, actual, sample });
|
||||
const tag = width + "/" + theme;
|
||||
ok(
|
||||
tag + " head width/height/name x",
|
||||
actual.head.width === sample.head.width &&
|
||||
actual.head.height === sample.head.height &&
|
||||
actual.name.x === sample.name.x,
|
||||
);
|
||||
ok(
|
||||
tag + " glass/bottom line/blur",
|
||||
actual.head.background === sample.head.background &&
|
||||
actual.head.bottomBorder === sample.head.bottomBorder &&
|
||||
actual.head.blur === sample.head.blur,
|
||||
);
|
||||
ok(
|
||||
tag + " search/input width",
|
||||
Math.abs(actual.search.width - sample.search.width) < 0.1 &&
|
||||
actual.box.width === sample.box.width,
|
||||
);
|
||||
ok(
|
||||
tag + " kbd exact appearance",
|
||||
[
|
||||
"fontSize",
|
||||
"fontWeight",
|
||||
"lineHeight",
|
||||
"padding",
|
||||
"radius",
|
||||
"border",
|
||||
"background",
|
||||
"color",
|
||||
].every((k) => actual.key[k] === sample.key[k]) &&
|
||||
actual.key.text === "/",
|
||||
);
|
||||
ok(
|
||||
tag + " filename breakpoint",
|
||||
width <= 1366
|
||||
? actual.file.width === 0 && sample.file.width === 0
|
||||
: actual.file.width > 0 && sample.file.width > 0,
|
||||
);
|
||||
ok(tag + " no overflow", !actual.overflow);
|
||||
await p.screenshot({ path: out + `/${width}-${theme}-actual.png` });
|
||||
await s.screenshot({ path: out + `/${width}-${theme}-sample.png` });
|
||||
}
|
||||
}
|
||||
for (const width of [1367, 1366]) {
|
||||
await p.setViewportSize({ width, height: 768 });
|
||||
await s.setViewportSize({ width, height: 768 });
|
||||
const a = await metrics(p),
|
||||
v = await metrics(s, true);
|
||||
ok(
|
||||
width + " boundary filename matches sample",
|
||||
(a.file.width === 0) === (v.file.width === 0),
|
||||
);
|
||||
}
|
||||
await p.setViewportSize({ width: 1440, height: 900 });
|
||||
const search = p.getByRole("searchbox", { name: "搜索工作流" }),
|
||||
origin = p.getByRole("tab", { name: "图片", exact: true });
|
||||
await origin.focus();
|
||||
await p.keyboard.press("/");
|
||||
ok(
|
||||
"/ focuses search",
|
||||
await search.evaluate((e) => e === document.activeElement),
|
||||
);
|
||||
await search.fill("遮罩");
|
||||
await pause(200);
|
||||
ok("search filters", (await p.locator(".workflow-pick").count()) === 1);
|
||||
await p.keyboard.press("Escape");
|
||||
ok(
|
||||
"Esc returns prior focus",
|
||||
await origin.evaluate((e) => e === document.activeElement),
|
||||
);
|
||||
ok("Esc preserves filter", (await search.inputValue()) === "遮罩");
|
||||
await search.fill("");
|
||||
await search.focus();
|
||||
await p.keyboard.press("/");
|
||||
ok(
|
||||
"search typing slash remains literal",
|
||||
(await search.inputValue()) === "/",
|
||||
);
|
||||
await search.fill("");
|
||||
const prompt = p.locator("#desk-prompt");
|
||||
await prompt.fill("");
|
||||
await prompt.focus();
|
||||
await p.keyboard.press("/");
|
||||
ok("textarea keeps slash", (await prompt.inputValue()) === "/");
|
||||
await p.evaluate(() => {
|
||||
let e = document.createElement("div");
|
||||
e.id = "editable-probe";
|
||||
e.contentEditable = "true";
|
||||
e.textContent = "IME";
|
||||
document.body.append(e);
|
||||
e.focus();
|
||||
});
|
||||
await p.keyboard.press("/");
|
||||
ok(
|
||||
"contenteditable keeps focus",
|
||||
await p
|
||||
.locator("#editable-probe")
|
||||
.evaluate(
|
||||
(e) => document.activeElement === e && e.textContent.includes("/"),
|
||||
),
|
||||
);
|
||||
await p.locator("#editable-probe").evaluate((e) => e.remove());
|
||||
await origin.focus();
|
||||
ok(
|
||||
"composition slash ignored",
|
||||
await origin.evaluate((e) => {
|
||||
let k = new KeyboardEvent("keydown", {
|
||||
key: "/",
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
isComposing: true,
|
||||
});
|
||||
e.dispatchEvent(k);
|
||||
return document.activeElement === e && !k.defaultPrevented;
|
||||
}),
|
||||
);
|
||||
ok(
|
||||
"legacy IME 229 ignored",
|
||||
await origin.evaluate((e) => {
|
||||
let k = new KeyboardEvent("keydown", {
|
||||
key: "/",
|
||||
keyCode: 229,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
e.dispatchEvent(k);
|
||||
return document.activeElement === e && !k.defaultPrevented;
|
||||
}),
|
||||
);
|
||||
await p.keyboard.press("Control+/");
|
||||
ok(
|
||||
"modified slash ignored",
|
||||
await origin.evaluate((e) => document.activeElement === e),
|
||||
);
|
||||
await prompt.focus();
|
||||
ok(
|
||||
"composition Escape keeps editor focus",
|
||||
await prompt.evaluate((e) => {
|
||||
let k = new KeyboardEvent("keydown", {
|
||||
key: "Escape",
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
isComposing: true,
|
||||
});
|
||||
e.dispatchEvent(k);
|
||||
return document.activeElement === e && !k.defaultPrevented;
|
||||
}),
|
||||
);
|
||||
await p.setViewportSize({ width: 1440, height: 900 });
|
||||
ok(
|
||||
"1440 filename is genuine source basename",
|
||||
(await p.locator(".workflow-file").textContent()) ===
|
||||
path.basename(source) &&
|
||||
(await p.locator(".workflow-file").getAttribute("title")) === source,
|
||||
);
|
||||
// Mouse focus and removal of the prior node use a stable rail control fallback.
|
||||
await search.click();
|
||||
await p.keyboard.press("Escape");
|
||||
ok(
|
||||
"mouse search Esc returns control",
|
||||
await p.evaluate(
|
||||
() =>
|
||||
document.activeElement !==
|
||||
document.querySelector(".rail-search input"),
|
||||
),
|
||||
);
|
||||
await p
|
||||
.getByRole("button", { name: "创作设置⌄", exact: true })
|
||||
.first()
|
||||
.click();
|
||||
await p.locator(".modal-panel").waitFor();
|
||||
await p.locator(".modal-panel button").first().focus();
|
||||
await p.keyboard.press("/");
|
||||
ok(
|
||||
"dialog slash never jumps to rail",
|
||||
!(await search.evaluate((e) => e === document.activeElement)),
|
||||
);
|
||||
await p.keyboard.press("Escape");
|
||||
ok(
|
||||
"dialog Escape closes normally",
|
||||
(await p.locator(".modal-panel").count()) === 0,
|
||||
);
|
||||
await p.getByRole("button", { name: "收起历史栏", exact: true }).focus();
|
||||
await p.keyboard.press("/");
|
||||
await p.keyboard.press("Escape");
|
||||
ok(
|
||||
"keyboard origin restored exactly",
|
||||
await p
|
||||
.getByRole("button", { name: "收起历史栏", exact: true })
|
||||
.evaluate((e) => document.activeElement === e),
|
||||
);
|
||||
ok("no browser errors", errors.length === 0);
|
||||
fs.writeFileSync(
|
||||
out + "/results.json",
|
||||
JSON.stringify({ checks, measurements, errors }, null, 2),
|
||||
);
|
||||
} finally {
|
||||
await b.close();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Run the isolated regression stack in the foreground and always stop children."""
|
||||
import os, signal, socket, subprocess, time, urllib.request
|
||||
from pathlib import Path
|
||||
root=Path(__file__).resolve().parents[1]
|
||||
os.chdir(root)
|
||||
py=str(root/'calliope-backend/.venv/bin/python')
|
||||
logs=root/'e2e/shots/hel728'; logs.mkdir(parents=True,exist_ok=True)
|
||||
env=os.environ.copy()
|
||||
for port in [8247,5173,18188,18189]:
|
||||
with socket.socket() as probe:
|
||||
assert probe.connect_ex(('127.0.0.1',port)) != 0, f'Test port {port} is already in use'
|
||||
# Every run gets fresh disposable state; retain prior evidence instead of deleting it.
|
||||
reference=root/'e2e/shots/reference'
|
||||
if reference.exists():reference.rename(reference.with_name('reference.previous-'+str(time.time_ns())))
|
||||
processes=[]; handles=[]
|
||||
try:
|
||||
for name,cmd in [('api',[py,'e2e/reference/server.py']),('comfy',[py,'e2e/fake_comfy.py']),('llm',[py,'e2e/reference/fake_llm.py']),('web',['npm','--prefix',os.environ.get('HEL728_WEB_DIR','calliope-web'),'run','dev'])]:
|
||||
h=open(logs/(name+'.log'),'w');handles.append(h);processes.append(subprocess.Popen(cmd,stdout=h,stderr=subprocess.STDOUT,env=env,start_new_session=True))
|
||||
for url in ['http://127.0.0.1:8247/api/health','http://127.0.0.1:5173']:
|
||||
for i in range(100):
|
||||
try:
|
||||
urllib.request.urlopen(url,timeout=2); break
|
||||
except Exception:
|
||||
if any(p.poll() is not None for p in processes): raise RuntimeError('A test service failed')
|
||||
if i==99:raise
|
||||
time.sleep(.2)
|
||||
for cmd in [[py,'e2e/seed.py'],[py,'e2e/reference/seed.py']]:subprocess.run(cmd,env=env,check=True)
|
||||
failures=[]
|
||||
scripts=os.environ.get('HEL728_TESTS','e2e/reference/check.cjs,e2e/reference/check-copy-fallback.cjs,e2e/hel728.cjs').split(',')
|
||||
for script in scripts:
|
||||
print('RUN',script,flush=True)
|
||||
result=subprocess.run(['node',script],env=env)
|
||||
if result.returncode:failures.append(script)
|
||||
print('FAILED SCRIPTS',failures,flush=True)
|
||||
if failures:raise SystemExit(1)
|
||||
|
||||
finally:
|
||||
for p in processes:
|
||||
if p.poll() is None:os.killpg(p.pid,signal.SIGTERM)
|
||||
for p in processes:
|
||||
try:p.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:os.killpg(p.pid,signal.SIGKILL);p.wait()
|
||||
for h in handles:h.close()
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Run the isolated regression stack in the foreground and always stop children."""
|
||||
import os, signal, socket, subprocess, time, urllib.request
|
||||
from pathlib import Path
|
||||
root=Path(__file__).resolve().parents[1]
|
||||
os.chdir(root)
|
||||
py=str(root/'calliope-backend/.venv/bin/python')
|
||||
logs=root/'e2e/shots/hel729'; logs.mkdir(parents=True,exist_ok=True)
|
||||
env=os.environ.copy()
|
||||
env['PYTHONPATH']=str(root/'calliope-backend/src')
|
||||
for port in [8247,5173,18188,18189]:
|
||||
with socket.socket() as probe:
|
||||
assert probe.connect_ex(('127.0.0.1',port)) != 0, f'Test port {port} is already in use'
|
||||
# Every run gets fresh disposable state; retain prior evidence instead of deleting it.
|
||||
reference=root/'e2e/shots/reference'
|
||||
if reference.exists():reference.rename(reference.with_name('reference.previous-'+str(time.time_ns())))
|
||||
processes=[]; handles=[]
|
||||
try:
|
||||
for name,cmd in [('api',[py,'e2e/reference/server.py']),('comfy',[py,'e2e/fake_comfy.py']),('llm',[py,'e2e/reference/fake_llm.py']),('web',['npm','--prefix',os.environ.get('HEL729_WEB_DIR','calliope-web'),'run','dev'])]:
|
||||
h=open(logs/(name+'.log'),'w');handles.append(h);processes.append(subprocess.Popen(cmd,stdout=h,stderr=subprocess.STDOUT,env=env,start_new_session=True))
|
||||
for url in ['http://127.0.0.1:8247/api/health','http://127.0.0.1:5173']:
|
||||
for i in range(100):
|
||||
try:
|
||||
urllib.request.urlopen(url,timeout=2); break
|
||||
except Exception:
|
||||
if any(p.poll() is not None for p in processes): raise RuntimeError('A test service failed')
|
||||
if i==99:raise
|
||||
time.sleep(.2)
|
||||
for cmd in [[py,'e2e/seed.py'],[py,'e2e/reference/seed.py']]:subprocess.run(cmd,env=env,check=True)
|
||||
failures=[]
|
||||
scripts=os.environ.get('HEL729_TESTS','e2e/hel729.cjs').split(',')
|
||||
for script in scripts:
|
||||
print('RUN',script,flush=True)
|
||||
result=subprocess.run(['node',script],env=env)
|
||||
if result.returncode:failures.append(script)
|
||||
print('FAILED SCRIPTS',failures,flush=True)
|
||||
if failures:raise SystemExit(1)
|
||||
|
||||
finally:
|
||||
for p in processes:
|
||||
if p.poll() is None:os.killpg(p.pid,signal.SIGTERM)
|
||||
for p in processes:
|
||||
try:p.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:os.killpg(p.pid,signal.SIGKILL);p.wait()
|
||||
for h in handles:h.close()
|
||||
Reference in New Issue
Block a user