mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
fix(web): remove generating-state takeover from the workspace preview
Drop the GenerationPreviewStage card that hijacked the right-side preview during generation/awaiting/stopped/failed phases. The workspace now stays on its original panel (Design Files browser / live preview) throughout a run, so generation no longer swaps the surface out from under the user. Removes the component, its CSS module, the generation-preview runtime model + tests, the now-unused FileWorkspace props (messages/artifactHtml/ conversationError/onRetry), and the generationPreview.* i18n keys across all locales.
This commit is contained in:
parent
9bb787e173
commit
f875f542c3
26 changed files with 0 additions and 1387 deletions
|
|
@ -60,9 +60,6 @@ import {
|
||||||
parseSketchWorkspaceDocument,
|
parseSketchWorkspaceDocument,
|
||||||
type SketchItem,
|
type SketchItem,
|
||||||
} from './sketch-model';
|
} from './sketch-model';
|
||||||
import { GenerationPreviewStage } from './GenerationPreviewStage';
|
|
||||||
import { buildGenerationPreviewState } from '../runtime/generation-preview';
|
|
||||||
import type { ChatMessage } from '../types';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|
@ -113,10 +110,6 @@ interface Props {
|
||||||
githubConnected?: boolean;
|
githubConnected?: boolean;
|
||||||
commentPortalId?: string;
|
commentPortalId?: string;
|
||||||
onCommentModeChange?: (active: boolean) => void;
|
onCommentModeChange?: (active: boolean) => void;
|
||||||
messages?: ChatMessage[];
|
|
||||||
artifactHtml?: string | null;
|
|
||||||
conversationError?: string | null;
|
|
||||||
onRetry?: (message: ChatMessage) => void;
|
|
||||||
// Active discovery question form, surfaced in the right-hand Questions tab
|
// Active discovery question form, surfaced in the right-hand Questions tab
|
||||||
// instead of inline in the chat. Owned by ProjectView (derived from the
|
// instead of inline in the chat. Owned by ProjectView (derived from the
|
||||||
// latest assistant message).
|
// latest assistant message).
|
||||||
|
|
@ -255,10 +248,6 @@ export function FileWorkspace({
|
||||||
githubConnected,
|
githubConnected,
|
||||||
commentPortalId,
|
commentPortalId,
|
||||||
onCommentModeChange,
|
onCommentModeChange,
|
||||||
messages = [],
|
|
||||||
artifactHtml,
|
|
||||||
conversationError,
|
|
||||||
onRetry,
|
|
||||||
questionForm = null,
|
questionForm = null,
|
||||||
questionFormPreview = null,
|
questionFormPreview = null,
|
||||||
questionFormKey = null,
|
questionFormKey = null,
|
||||||
|
|
@ -313,21 +302,6 @@ export function FileWorkspace({
|
||||||
[liveArtifacts],
|
[liveArtifacts],
|
||||||
);
|
);
|
||||||
|
|
||||||
const generationPreview = useMemo(
|
|
||||||
() =>
|
|
||||||
buildGenerationPreviewState({
|
|
||||||
designSystemProject: Boolean(designSystemProject),
|
|
||||||
messages,
|
|
||||||
streaming: Boolean(streaming),
|
|
||||||
activeTab,
|
|
||||||
projectFiles: visibleFiles,
|
|
||||||
liveArtifacts,
|
|
||||||
artifactHtml,
|
|
||||||
conversationError,
|
|
||||||
}),
|
|
||||||
[designSystemProject, messages, streaming, activeTab, visibleFiles, liveArtifacts, artifactHtml, conversationError],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Pull the persisted active tab in when the parent's hydration completes
|
// Pull the persisted active tab in when the parent's hydration completes
|
||||||
// (or on project switch). Fall back to the Design Files browser so a
|
// (or on project switch). Fall back to the Design Files browser so a
|
||||||
// fresh project lands in a useful place.
|
// fresh project lands in a useful place.
|
||||||
|
|
@ -1076,15 +1050,6 @@ export function FileWorkspace({
|
||||||
onConnectRepo={onConnectRepo}
|
onConnectRepo={onConnectRepo}
|
||||||
githubConnected={githubConnected}
|
githubConnected={githubConnected}
|
||||||
/>
|
/>
|
||||||
) : generationPreview ? (
|
|
||||||
<GenerationPreviewStage
|
|
||||||
model={generationPreview}
|
|
||||||
onRetry={
|
|
||||||
generationPreview.retryTarget && onRetry
|
|
||||||
? () => onRetry(generationPreview.retryTarget!)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : activeTab === DESIGN_FILES_TAB ? (
|
) : activeTab === DESIGN_FILES_TAB ? (
|
||||||
<DesignFilesPanel
|
<DesignFilesPanel
|
||||||
key={projectId}
|
key={projectId}
|
||||||
|
|
|
||||||
|
|
@ -1,281 +0,0 @@
|
||||||
.stage {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 14px;
|
|
||||||
min-height: 100%;
|
|
||||||
padding: 48px 32px;
|
|
||||||
text-align: center;
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mark {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 52px;
|
|
||||||
height: 52px;
|
|
||||||
border-radius: 14px;
|
|
||||||
border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border));
|
|
||||||
background: color-mix(in srgb, var(--accent) 8%, var(--bg-panel));
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* A gentle breathing halo so the card reads as "alive" while we wait,
|
|
||||||
even between discrete progress events. */
|
|
||||||
.mark[data-active='true'] {
|
|
||||||
animation: markBreathe 2.4s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage[data-phase='failed'] .mark {
|
|
||||||
border-color: color-mix(in srgb, var(--red) 32%, var(--border));
|
|
||||||
background: color-mix(in srgb, var(--red) 8%, var(--bg-panel));
|
|
||||||
color: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage[data-phase='stopped'] .mark {
|
|
||||||
border-color: var(--border);
|
|
||||||
background: var(--bg-panel);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes markBreathe {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 22%, transparent);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.04);
|
|
||||||
box-shadow: 0 0 0 8px color-mix(in srgb, var(--accent) 0%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lead {
|
|
||||||
margin: 0;
|
|
||||||
max-width: 420px;
|
|
||||||
min-height: 21px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Live activity snippet: subtly de-emphasised + animated so streaming
|
|
||||||
updates feel continuous rather than abrupt. */
|
|
||||||
.lead[data-live='true'] {
|
|
||||||
color: var(--text-faint);
|
|
||||||
animation: leadFade 240ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes leadFade {
|
|
||||||
from {
|
|
||||||
opacity: 0.35;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
position: relative;
|
|
||||||
width: min(360px, 100%);
|
|
||||||
height: 4px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--border-soft);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress span {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
min-width: 8px;
|
|
||||||
border-radius: inherit;
|
|
||||||
background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 24%, transparent));
|
|
||||||
transition: width 200ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Indeterminate shimmer sweeping over the determinate fill so the bar
|
|
||||||
keeps moving even when the percentage holds steady between events. */
|
|
||||||
.progress[data-active='true']::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
border-radius: inherit;
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
transparent,
|
|
||||||
color-mix(in srgb, var(--accent) 45%, transparent),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
transform: translateX(-100%);
|
|
||||||
animation: progressSweep 1.5s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage[data-phase='failed'] .progress span {
|
|
||||||
background: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes progressSweep {
|
|
||||||
0% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dynamic sub-status for the long "generating" step: the concrete task
|
|
||||||
plus a count, refreshed as the agent advances so the middle phase keeps
|
|
||||||
moving without splitting into more (less reliable) discrete steps. */
|
|
||||||
.substatus {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
max-width: 420px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-faint);
|
|
||||||
animation: leadFade 240ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.substatusLabel {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.substatusCount {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 1px 7px;
|
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: var(--bg-panel);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
min-height: 28px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
background: var(--bg-panel);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
/* Steps are revealed one at a time as the agent reaches them, so each
|
|
||||||
pill slides + fades in on mount to make the progression visible. */
|
|
||||||
animation: stepReveal 280ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes stepReveal {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(4px) scale(0.96);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.step[data-status='running'] {
|
|
||||||
border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step[data-status='succeeded'] {
|
|
||||||
border-color: color-mix(in srgb, var(--green) 36%, var(--border));
|
|
||||||
color: var(--green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step[data-status='failed'] {
|
|
||||||
border-color: color-mix(in srgb, var(--red) 40%, var(--border));
|
|
||||||
color: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stepIcon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stepDot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: currentColor;
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stepDot[data-running='true'] {
|
|
||||||
opacity: 1;
|
|
||||||
animation: stepPulse 1.1s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes stepPulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.5);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.mark[data-active='true'],
|
|
||||||
.progress[data-active='true']::after,
|
|
||||||
.stepDot[data-running='true'],
|
|
||||||
.lead[data-live='true'],
|
|
||||||
.step,
|
|
||||||
.substatus {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry {
|
|
||||||
margin-top: 4px;
|
|
||||||
min-height: 34px;
|
|
||||||
padding: 0 16px;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--accent-contrast, #fff);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 140ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry:hover {
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry:focus-visible {
|
|
||||||
outline: 2px solid var(--accent);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
import { useT } from '../i18n';
|
|
||||||
import type { GenerationPreviewModel } from '../runtime/generation-preview';
|
|
||||||
import { Icon } from './Icon';
|
|
||||||
import styles from './GenerationPreviewStage.module.css';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
model: GenerationPreviewModel;
|
|
||||||
onRetry?: (() => void) | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function GenerationPreviewStage({ model, onRetry }: Props) {
|
|
||||||
const t = useT();
|
|
||||||
|
|
||||||
const generating = model.phase === 'generating';
|
|
||||||
|
|
||||||
const stepLabels: Record<GenerationPreviewModel['steps'][number]['id'], string> = {
|
|
||||||
understand: t('generationPreview.stepUnderstand'),
|
|
||||||
generate: t('generationPreview.stepGenerate'),
|
|
||||||
prepare: t('generationPreview.stepPrepare'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const title =
|
|
||||||
model.phase === 'failed'
|
|
||||||
? t('generationPreview.failedTitle')
|
|
||||||
: model.phase === 'stopped'
|
|
||||||
? t('generationPreview.stoppedTitle')
|
|
||||||
: model.phase === 'awaiting-input'
|
|
||||||
? t('generationPreview.awaitingTitle')
|
|
||||||
: t('generationPreview.title');
|
|
||||||
|
|
||||||
const lead =
|
|
||||||
model.phase === 'failed'
|
|
||||||
? model.errorMessage || t('generationPreview.failedFallback')
|
|
||||||
: model.phase === 'stopped'
|
|
||||||
? t('generationPreview.stoppedLead')
|
|
||||||
: model.phase === 'awaiting-input'
|
|
||||||
? t('generationPreview.awaitingLead')
|
|
||||||
: model.activityLabel;
|
|
||||||
|
|
||||||
const markIcon =
|
|
||||||
model.phase === 'failed' ? 'close' : model.phase === 'stopped' ? 'stop' : 'sparkles';
|
|
||||||
|
|
||||||
// Once concrete sub-status (current task + count) is available we let it
|
|
||||||
// carry the live signal and drop the higher-level narration line, so only
|
|
||||||
// one dynamic line shows at a time.
|
|
||||||
const showSubstatus = generating && Boolean(model.detailLabel || model.todoProgress);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
className={styles.stage}
|
|
||||||
data-testid="generation-preview-stage"
|
|
||||||
data-phase={model.phase}
|
|
||||||
aria-live="polite"
|
|
||||||
aria-busy={generating}
|
|
||||||
>
|
|
||||||
<div className={styles.mark} data-active={generating} aria-hidden>
|
|
||||||
<Icon name={markIcon} size={24} />
|
|
||||||
</div>
|
|
||||||
<h1 className={styles.title}>{title}</h1>
|
|
||||||
{!showSubstatus && lead ? (
|
|
||||||
<p className={styles.lead} data-live={generating && Boolean(model.activityLabel)}>
|
|
||||||
{lead}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
<div
|
|
||||||
className={styles.progress}
|
|
||||||
data-active={generating}
|
|
||||||
role="progressbar"
|
|
||||||
aria-label={t('generationPreview.progressAria', { percent: model.progressPercent })}
|
|
||||||
aria-valuemin={0}
|
|
||||||
aria-valuemax={100}
|
|
||||||
aria-valuenow={model.progressPercent}
|
|
||||||
>
|
|
||||||
<span style={{ width: `${model.progressPercent}%` }} />
|
|
||||||
</div>
|
|
||||||
<ol className={styles.steps}>
|
|
||||||
{model.steps
|
|
||||||
.filter((step) => step.status !== 'pending')
|
|
||||||
.map((step) => (
|
|
||||||
<li key={step.id} className={styles.step} data-status={step.status}>
|
|
||||||
<span className={styles.stepIcon} aria-hidden>
|
|
||||||
{step.status === 'succeeded' ? (
|
|
||||||
<Icon name="check" size={12} />
|
|
||||||
) : step.status === 'failed' ? (
|
|
||||||
<Icon name="close" size={12} />
|
|
||||||
) : (
|
|
||||||
<span className={styles.stepDot} data-running={step.status === 'running' && generating} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className={styles.stepLabel}>{stepLabels[step.id]}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
{generating && (model.detailLabel || model.todoProgress) ? (
|
|
||||||
<div
|
|
||||||
key={`${model.detailLabel ?? ''}-${model.todoProgress?.done ?? ''}`}
|
|
||||||
className={styles.substatus}
|
|
||||||
>
|
|
||||||
{model.detailLabel ? (
|
|
||||||
<span className={styles.substatusLabel}>{model.detailLabel}</span>
|
|
||||||
) : null}
|
|
||||||
{model.todoProgress ? (
|
|
||||||
<span className={styles.substatusCount}>
|
|
||||||
{model.todoProgress.done}/{model.todoProgress.total}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{model.phase === 'failed' && onRetry ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.retry}
|
|
||||||
data-testid="generation-preview-retry"
|
|
||||||
onClick={onRetry}
|
|
||||||
>
|
|
||||||
{t('generationPreview.retry')}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -4660,10 +4660,6 @@ export function ProjectView({
|
||||||
githubConnected={githubConnected}
|
githubConnected={githubConnected}
|
||||||
commentPortalId={commentInspectorPortalId}
|
commentPortalId={commentInspectorPortalId}
|
||||||
onCommentModeChange={setCommentInspectorActive}
|
onCommentModeChange={setCommentInspectorActive}
|
||||||
messages={messages}
|
|
||||||
artifactHtml={artifact?.html}
|
|
||||||
conversationError={error}
|
|
||||||
onRetry={handleRetry}
|
|
||||||
questionForm={questionForm}
|
questionForm={questionForm}
|
||||||
questionFormPreview={questionFormPreview}
|
questionFormPreview={questionFormPreview}
|
||||||
questionFormKey={questionFormKey}
|
questionFormKey={questionFormKey}
|
||||||
|
|
|
||||||
|
|
@ -1816,19 +1816,4 @@ export const ar: Dict = {
|
||||||
'diagnostics.exporting': 'جارٍ التصدير…',
|
'diagnostics.exporting': 'جارٍ التصدير…',
|
||||||
'diagnostics.exportSuccess': 'تم حفظ التشخيص في {path}',
|
'diagnostics.exportSuccess': 'تم حفظ التشخيص في {path}',
|
||||||
'diagnostics.exportFailed': 'تعذّر تصدير التشخيص: {message}',
|
'diagnostics.exportFailed': 'تعذّر تصدير التشخيص: {message}',
|
||||||
'generationPreview.title': 'جارٍ الإنشاء…',
|
|
||||||
'generationPreview.failedTitle': 'فشل الإنشاء',
|
|
||||||
'generationPreview.failedFallback': 'حدث خطأ ما. يرجى المحاولة مرة أخرى.',
|
|
||||||
'generationPreview.footnote': 'يستغرق عادةً من 2 إلى 5 دقائق',
|
|
||||||
'generationPreview.stepUnderstand': 'فهم المتطلبات',
|
|
||||||
'generationPreview.stepGenerate': 'إنشاء الصفحة',
|
|
||||||
'generationPreview.stepPrepare': 'تحضير المعاينة',
|
|
||||||
'generationPreview.elapsed': 'مضى {elapsed}',
|
|
||||||
'generationPreview.estimate': 'عادةً 2–5 دقائق',
|
|
||||||
'generationPreview.progressAria': 'تقدّم الإنشاء: {percent}%',
|
|
||||||
'generationPreview.retry': 'إعادة المحاولة',
|
|
||||||
'generationPreview.awaitingTitle': 'في انتظار ردّك',
|
|
||||||
'generationPreview.awaitingLead': 'أجب عن بعض الأسئلة في المحادثة للمتابعة.',
|
|
||||||
'generationPreview.stoppedTitle': 'تم إيقاف الإنشاء مؤقتًا',
|
|
||||||
'generationPreview.stoppedLead': 'تابع الخطوات المتبقية من المحادثة على اليسار.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1753,19 +1753,4 @@ export const de: Dict = {
|
||||||
'diagnostics.exporting': 'Exportiere…',
|
'diagnostics.exporting': 'Exportiere…',
|
||||||
'diagnostics.exportSuccess': 'Diagnose gespeichert: {path}',
|
'diagnostics.exportSuccess': 'Diagnose gespeichert: {path}',
|
||||||
'diagnostics.exportFailed': 'Diagnose-Export fehlgeschlagen: {message}',
|
'diagnostics.exportFailed': 'Diagnose-Export fehlgeschlagen: {message}',
|
||||||
'generationPreview.title': 'Wird generiert…',
|
|
||||||
'generationPreview.failedTitle': 'Generierung fehlgeschlagen',
|
|
||||||
'generationPreview.failedFallback': 'Etwas ist schiefgelaufen. Bitte versuche es erneut.',
|
|
||||||
'generationPreview.footnote': 'Dauert normalerweise 2–5 Minuten',
|
|
||||||
'generationPreview.stepUnderstand': 'Anforderungen verstehen',
|
|
||||||
'generationPreview.stepGenerate': 'Seite generieren',
|
|
||||||
'generationPreview.stepPrepare': 'Vorschau vorbereiten',
|
|
||||||
'generationPreview.elapsed': '{elapsed} vergangen',
|
|
||||||
'generationPreview.estimate': 'Normalerweise 2–5 Min.',
|
|
||||||
'generationPreview.progressAria': 'Generierungsfortschritt: {percent}%',
|
|
||||||
'generationPreview.retry': 'Erneut versuchen',
|
|
||||||
'generationPreview.awaitingTitle': 'Warten auf deine Eingabe',
|
|
||||||
'generationPreview.awaitingLead': 'Beantworte ein paar kurze Fragen im Chat, um fortzufahren.',
|
|
||||||
'generationPreview.stoppedTitle': 'Generierung pausiert',
|
|
||||||
'generationPreview.stoppedLead': 'Setze die verbleibenden Schritte im Chat links fort.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1577,21 +1577,6 @@ export const en: Dict = {
|
||||||
'workspace.openFromDesignFiles': 'Open a file from',
|
'workspace.openFromDesignFiles': 'Open a file from',
|
||||||
'workspace.designFilesLink': 'Design Files',
|
'workspace.designFilesLink': 'Design Files',
|
||||||
'workspace.loadingSketch': 'Loading sketch…',
|
'workspace.loadingSketch': 'Loading sketch…',
|
||||||
'generationPreview.title': 'Generating…',
|
|
||||||
'generationPreview.failedTitle': 'Generation failed',
|
|
||||||
'generationPreview.failedFallback': 'Something went wrong. Please try again.',
|
|
||||||
'generationPreview.footnote': 'Usually takes 2–5 minutes',
|
|
||||||
'generationPreview.stepUnderstand': 'Understanding requirements',
|
|
||||||
'generationPreview.stepGenerate': 'Generating page',
|
|
||||||
'generationPreview.stepPrepare': 'Preparing preview',
|
|
||||||
'generationPreview.elapsed': '{elapsed} elapsed',
|
|
||||||
'generationPreview.estimate': 'Usually 2–5 min',
|
|
||||||
'generationPreview.progressAria': 'Generation progress: {percent}%',
|
|
||||||
'generationPreview.retry': 'Retry',
|
|
||||||
'generationPreview.awaitingTitle': 'Waiting for your input',
|
|
||||||
'generationPreview.awaitingLead': 'Answer a few quick questions in the chat to continue.',
|
|
||||||
'generationPreview.stoppedTitle': 'Generation paused',
|
|
||||||
'generationPreview.stoppedLead': 'Resume the remaining steps from the chat on the left.',
|
|
||||||
'designFiles.title': 'Design Files',
|
'designFiles.title': 'Design Files',
|
||||||
'designFiles.upload': 'Upload files',
|
'designFiles.upload': 'Upload files',
|
||||||
'designFiles.pasteText': 'Paste as text file',
|
'designFiles.pasteText': 'Paste as text file',
|
||||||
|
|
|
||||||
|
|
@ -1704,19 +1704,4 @@ export const esES: Dict = {
|
||||||
'diagnostics.exporting': 'Exportando…',
|
'diagnostics.exporting': 'Exportando…',
|
||||||
'diagnostics.exportSuccess': 'Diagnósticos guardados en {path}',
|
'diagnostics.exportSuccess': 'Diagnósticos guardados en {path}',
|
||||||
'diagnostics.exportFailed': 'No se pudieron exportar los diagnósticos: {message}',
|
'diagnostics.exportFailed': 'No se pudieron exportar los diagnósticos: {message}',
|
||||||
'generationPreview.title': 'Generando…',
|
|
||||||
'generationPreview.failedTitle': 'Error de generación',
|
|
||||||
'generationPreview.failedFallback': 'Algo salió mal. Inténtalo de nuevo.',
|
|
||||||
'generationPreview.footnote': 'Suele tardar de 2 a 5 minutos',
|
|
||||||
'generationPreview.stepUnderstand': 'Entendiendo los requisitos',
|
|
||||||
'generationPreview.stepGenerate': 'Generando la página',
|
|
||||||
'generationPreview.stepPrepare': 'Preparando la vista previa',
|
|
||||||
'generationPreview.elapsed': '{elapsed} transcurridos',
|
|
||||||
'generationPreview.estimate': 'Normalmente 2–5 min',
|
|
||||||
'generationPreview.progressAria': 'Progreso de la generación: {percent}%',
|
|
||||||
'generationPreview.retry': 'Reintentar',
|
|
||||||
'generationPreview.awaitingTitle': 'Esperando tu respuesta',
|
|
||||||
'generationPreview.awaitingLead': 'Responde unas preguntas en el chat para continuar.',
|
|
||||||
'generationPreview.stoppedTitle': 'Generación en pausa',
|
|
||||||
'generationPreview.stoppedLead': 'Reanuda los pasos restantes desde el chat de la izquierda.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1858,19 +1858,4 @@ export const fa: Dict = {
|
||||||
'diagnostics.exporting': 'در حال صادر کردن…',
|
'diagnostics.exporting': 'در حال صادر کردن…',
|
||||||
'diagnostics.exportSuccess': 'تشخیص در {path} ذخیره شد',
|
'diagnostics.exportSuccess': 'تشخیص در {path} ذخیره شد',
|
||||||
'diagnostics.exportFailed': 'صادر کردن تشخیص ناموفق بود: {message}',
|
'diagnostics.exportFailed': 'صادر کردن تشخیص ناموفق بود: {message}',
|
||||||
'generationPreview.title': 'در حال ساخت…',
|
|
||||||
'generationPreview.failedTitle': 'ساخت ناموفق بود',
|
|
||||||
'generationPreview.failedFallback': 'مشکلی پیش آمد. لطفاً دوباره تلاش کنید.',
|
|
||||||
'generationPreview.footnote': 'معمولاً ۲ تا ۵ دقیقه طول میکشد',
|
|
||||||
'generationPreview.stepUnderstand': 'درک نیازمندیها',
|
|
||||||
'generationPreview.stepGenerate': 'ساخت صفحه',
|
|
||||||
'generationPreview.stepPrepare': 'آمادهسازی پیشنمایش',
|
|
||||||
'generationPreview.elapsed': '{elapsed} گذشته',
|
|
||||||
'generationPreview.estimate': 'معمولاً ۲ تا ۵ دقیقه',
|
|
||||||
'generationPreview.progressAria': 'پیشرفت ساخت: {percent}%',
|
|
||||||
'generationPreview.retry': 'تلاش دوباره',
|
|
||||||
'generationPreview.awaitingTitle': 'در انتظار پاسخ شما',
|
|
||||||
'generationPreview.awaitingLead': 'برای ادامه، به چند پرسش در گفتگو پاسخ دهید.',
|
|
||||||
'generationPreview.stoppedTitle': 'ساخت متوقف شد',
|
|
||||||
'generationPreview.stoppedLead': 'مراحل باقیمانده را از گفتگوی سمت چپ ادامه دهید.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2465,19 +2465,4 @@ export const fr: Dict = {
|
||||||
'diagnostics.exporting': 'Exportation…',
|
'diagnostics.exporting': 'Exportation…',
|
||||||
'diagnostics.exportSuccess': 'Diagnostic enregistré dans {path}',
|
'diagnostics.exportSuccess': 'Diagnostic enregistré dans {path}',
|
||||||
'diagnostics.exportFailed': 'Impossible d\'exporter le diagnostic: {message}',
|
'diagnostics.exportFailed': 'Impossible d\'exporter le diagnostic: {message}',
|
||||||
'generationPreview.title': 'Génération…',
|
|
||||||
'generationPreview.failedTitle': 'Échec de la génération',
|
|
||||||
'generationPreview.failedFallback': 'Une erreur est survenue. Veuillez réessayer.',
|
|
||||||
'generationPreview.footnote': 'Prend généralement 2 à 5 minutes',
|
|
||||||
'generationPreview.stepUnderstand': 'Compréhension des besoins',
|
|
||||||
'generationPreview.stepGenerate': 'Génération de la page',
|
|
||||||
'generationPreview.stepPrepare': 'Préparation de l\'aperçu',
|
|
||||||
'generationPreview.elapsed': '{elapsed} écoulées',
|
|
||||||
'generationPreview.estimate': 'Généralement 2–5 min',
|
|
||||||
'generationPreview.progressAria': 'Progression de la génération : {percent}%',
|
|
||||||
'generationPreview.retry': 'Réessayer',
|
|
||||||
'generationPreview.awaitingTitle': 'En attente de votre réponse',
|
|
||||||
'generationPreview.awaitingLead': 'Répondez à quelques questions dans le chat pour continuer.',
|
|
||||||
'generationPreview.stoppedTitle': 'Génération en pause',
|
|
||||||
'generationPreview.stoppedLead': 'Reprenez les étapes restantes depuis le chat à gauche.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1825,19 +1825,4 @@ export const hu: Dict = {
|
||||||
'diagnostics.exporting': 'Exportálás…',
|
'diagnostics.exporting': 'Exportálás…',
|
||||||
'diagnostics.exportSuccess': 'Diagnosztika mentve: {path}',
|
'diagnostics.exportSuccess': 'Diagnosztika mentve: {path}',
|
||||||
'diagnostics.exportFailed': 'Diagnosztika exportálása sikertelen: {message}',
|
'diagnostics.exportFailed': 'Diagnosztika exportálása sikertelen: {message}',
|
||||||
'generationPreview.title': 'Generálás…',
|
|
||||||
'generationPreview.failedTitle': 'A generálás sikertelen',
|
|
||||||
'generationPreview.failedFallback': 'Hiba történt. Kérjük, próbáld újra.',
|
|
||||||
'generationPreview.footnote': 'Általában 2–5 percet vesz igénybe',
|
|
||||||
'generationPreview.stepUnderstand': 'Követelmények értelmezése',
|
|
||||||
'generationPreview.stepGenerate': 'Oldal generálása',
|
|
||||||
'generationPreview.stepPrepare': 'Előnézet előkészítése',
|
|
||||||
'generationPreview.elapsed': '{elapsed} eltelt',
|
|
||||||
'generationPreview.estimate': 'Általában 2–5 perc',
|
|
||||||
'generationPreview.progressAria': 'Generálás állapota: {percent}%',
|
|
||||||
'generationPreview.retry': 'Újra',
|
|
||||||
'generationPreview.awaitingTitle': 'Várakozás a válaszodra',
|
|
||||||
'generationPreview.awaitingLead': 'Válaszolj néhány kérdésre a csevegésben a folytatáshoz.',
|
|
||||||
'generationPreview.stoppedTitle': 'Generálás szüneteltetve',
|
|
||||||
'generationPreview.stoppedLead': 'Folytasd a hátralévő lépéseket a bal oldali csevegésből.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1858,19 +1858,4 @@ export const id: Dict = {
|
||||||
'diagnostics.exporting': 'Mengekspor…',
|
'diagnostics.exporting': 'Mengekspor…',
|
||||||
'diagnostics.exportSuccess': 'Diagnostik disimpan di {path}',
|
'diagnostics.exportSuccess': 'Diagnostik disimpan di {path}',
|
||||||
'diagnostics.exportFailed': 'Gagal mengekspor diagnostik: {message}',
|
'diagnostics.exportFailed': 'Gagal mengekspor diagnostik: {message}',
|
||||||
'generationPreview.title': 'Membuat…',
|
|
||||||
'generationPreview.failedTitle': 'Pembuatan gagal',
|
|
||||||
'generationPreview.failedFallback': 'Terjadi kesalahan. Silakan coba lagi.',
|
|
||||||
'generationPreview.footnote': 'Biasanya butuh 2–5 menit',
|
|
||||||
'generationPreview.stepUnderstand': 'Memahami kebutuhan',
|
|
||||||
'generationPreview.stepGenerate': 'Membuat halaman',
|
|
||||||
'generationPreview.stepPrepare': 'Menyiapkan pratinjau',
|
|
||||||
'generationPreview.elapsed': '{elapsed} berlalu',
|
|
||||||
'generationPreview.estimate': 'Biasanya 2–5 mnt',
|
|
||||||
'generationPreview.progressAria': 'Progres pembuatan: {percent}%',
|
|
||||||
'generationPreview.retry': 'Coba lagi',
|
|
||||||
'generationPreview.awaitingTitle': 'Menunggu masukan Anda',
|
|
||||||
'generationPreview.awaitingLead': 'Jawab beberapa pertanyaan di obrolan untuk melanjutkan.',
|
|
||||||
'generationPreview.stoppedTitle': 'Pembuatan dijeda',
|
|
||||||
'generationPreview.stoppedLead': 'Lanjutkan langkah yang tersisa dari obrolan di kiri.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1684,19 +1684,4 @@ export const it: Dict = {
|
||||||
'liveArtifact.viewer.code.loading': 'Caricamento codice…',
|
'liveArtifact.viewer.code.loading': 'Caricamento codice…',
|
||||||
'liveArtifact.viewer.code.unavailable': 'Il codice non è ancora disponibile.',
|
'liveArtifact.viewer.code.unavailable': 'Il codice non è ancora disponibile.',
|
||||||
'liveArtifact.viewer.code.empty': 'Questo file di codice è vuoto.',
|
'liveArtifact.viewer.code.empty': 'Questo file di codice è vuoto.',
|
||||||
'generationPreview.title': 'Generazione…',
|
|
||||||
'generationPreview.failedTitle': 'Generazione non riuscita',
|
|
||||||
'generationPreview.failedFallback': 'Qualcosa è andato storto. Riprova.',
|
|
||||||
'generationPreview.footnote': 'Di solito richiede 2–5 minuti',
|
|
||||||
'generationPreview.stepUnderstand': 'Analisi dei requisiti',
|
|
||||||
'generationPreview.stepGenerate': 'Generazione della pagina',
|
|
||||||
'generationPreview.stepPrepare': 'Preparazione dell\'anteprima',
|
|
||||||
'generationPreview.elapsed': '{elapsed} trascorsi',
|
|
||||||
'generationPreview.estimate': 'Di solito 2–5 min',
|
|
||||||
'generationPreview.progressAria': 'Avanzamento della generazione: {percent}%',
|
|
||||||
'generationPreview.retry': 'Riprova',
|
|
||||||
'generationPreview.awaitingTitle': 'In attesa della tua risposta',
|
|
||||||
'generationPreview.awaitingLead': 'Rispondi ad alcune domande nella chat per continuare.',
|
|
||||||
'generationPreview.stoppedTitle': 'Generazione in pausa',
|
|
||||||
'generationPreview.stoppedLead': 'Riprendi i passaggi rimanenti dalla chat a sinistra.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1752,19 +1752,4 @@ export const ja: Dict = {
|
||||||
'diagnostics.exporting': 'エクスポート中…',
|
'diagnostics.exporting': 'エクスポート中…',
|
||||||
'diagnostics.exportSuccess': '診断情報を {path} に保存しました',
|
'diagnostics.exportSuccess': '診断情報を {path} に保存しました',
|
||||||
'diagnostics.exportFailed': '診断情報のエクスポートに失敗しました: {message}',
|
'diagnostics.exportFailed': '診断情報のエクスポートに失敗しました: {message}',
|
||||||
'generationPreview.title': '生成中…',
|
|
||||||
'generationPreview.failedTitle': '生成に失敗しました',
|
|
||||||
'generationPreview.failedFallback': '問題が発生しました。もう一度お試しください。',
|
|
||||||
'generationPreview.footnote': '通常2〜5分かかります',
|
|
||||||
'generationPreview.stepUnderstand': '要件を理解中',
|
|
||||||
'generationPreview.stepGenerate': 'ページを生成中',
|
|
||||||
'generationPreview.stepPrepare': 'プレビューを準備中',
|
|
||||||
'generationPreview.elapsed': '経過 {elapsed}',
|
|
||||||
'generationPreview.estimate': '通常2〜5分',
|
|
||||||
'generationPreview.progressAria': '生成の進捗:{percent}%',
|
|
||||||
'generationPreview.retry': '再試行',
|
|
||||||
'generationPreview.awaitingTitle': '入力をお待ちしています',
|
|
||||||
'generationPreview.awaitingLead': 'チャットでいくつかの質問に答えると続行します。',
|
|
||||||
'generationPreview.stoppedTitle': '生成を一時停止しました',
|
|
||||||
'generationPreview.stoppedLead': '左側のチャットから残りのステップを再開できます。',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1865,19 +1865,4 @@ export const ko: Dict = {
|
||||||
'diagnostics.exporting': '내보내는 중…',
|
'diagnostics.exporting': '내보내는 중…',
|
||||||
'diagnostics.exportSuccess': '진단 정보를 {path}에 저장했습니다',
|
'diagnostics.exportSuccess': '진단 정보를 {path}에 저장했습니다',
|
||||||
'diagnostics.exportFailed': '진단 정보 내보내기 실패: {message}',
|
'diagnostics.exportFailed': '진단 정보 내보내기 실패: {message}',
|
||||||
'generationPreview.title': '생성 중…',
|
|
||||||
'generationPreview.failedTitle': '생성 실패',
|
|
||||||
'generationPreview.failedFallback': '문제가 발생했습니다. 다시 시도해 주세요.',
|
|
||||||
'generationPreview.footnote': '보통 2~5분 정도 걸립니다',
|
|
||||||
'generationPreview.stepUnderstand': '요구사항 이해 중',
|
|
||||||
'generationPreview.stepGenerate': '페이지 생성 중',
|
|
||||||
'generationPreview.stepPrepare': '미리보기 준비 중',
|
|
||||||
'generationPreview.elapsed': '경과 {elapsed}',
|
|
||||||
'generationPreview.estimate': '보통 2~5분',
|
|
||||||
'generationPreview.progressAria': '생성 진행률: {percent}%',
|
|
||||||
'generationPreview.retry': '다시 시도',
|
|
||||||
'generationPreview.awaitingTitle': '입력을 기다리는 중',
|
|
||||||
'generationPreview.awaitingLead': '왼쪽 채팅에서 몇 가지 질문에 답하면 계속됩니다.',
|
|
||||||
'generationPreview.stoppedTitle': '생성이 일시중지됨',
|
|
||||||
'generationPreview.stoppedLead': '왼쪽 채팅에서 남은 단계를 다시 시작할 수 있습니다.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1815,19 +1815,4 @@ export const pl: Dict = {
|
||||||
'diagnostics.exporting': 'Eksportowanie…',
|
'diagnostics.exporting': 'Eksportowanie…',
|
||||||
'diagnostics.exportSuccess': 'Diagnostyka zapisana w {path}',
|
'diagnostics.exportSuccess': 'Diagnostyka zapisana w {path}',
|
||||||
'diagnostics.exportFailed': 'Nie udało się wyeksportować diagnostyki: {message}',
|
'diagnostics.exportFailed': 'Nie udało się wyeksportować diagnostyki: {message}',
|
||||||
'generationPreview.title': 'Generowanie…',
|
|
||||||
'generationPreview.failedTitle': 'Generowanie nie powiodło się',
|
|
||||||
'generationPreview.failedFallback': 'Coś poszło nie tak. Spróbuj ponownie.',
|
|
||||||
'generationPreview.footnote': 'Zwykle trwa 2–5 minut',
|
|
||||||
'generationPreview.stepUnderstand': 'Analiza wymagań',
|
|
||||||
'generationPreview.stepGenerate': 'Generowanie strony',
|
|
||||||
'generationPreview.stepPrepare': 'Przygotowanie podglądu',
|
|
||||||
'generationPreview.elapsed': 'Upłynęło {elapsed}',
|
|
||||||
'generationPreview.estimate': 'Zwykle 2–5 min',
|
|
||||||
'generationPreview.progressAria': 'Postęp generowania: {percent}%',
|
|
||||||
'generationPreview.retry': 'Spróbuj ponownie',
|
|
||||||
'generationPreview.awaitingTitle': 'Oczekiwanie na Twoją odpowiedź',
|
|
||||||
'generationPreview.awaitingLead': 'Odpowiedz na kilka pytań na czacie, aby kontynuować.',
|
|
||||||
'generationPreview.stoppedTitle': 'Generowanie wstrzymane',
|
|
||||||
'generationPreview.stoppedLead': 'Wznów pozostałe kroki z czatu po lewej stronie.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1856,19 +1856,4 @@ export const ptBR: Dict = {
|
||||||
'diagnostics.exporting': 'Exportando…',
|
'diagnostics.exporting': 'Exportando…',
|
||||||
'diagnostics.exportSuccess': 'Diagnósticos salvos em {path}',
|
'diagnostics.exportSuccess': 'Diagnósticos salvos em {path}',
|
||||||
'diagnostics.exportFailed': 'Falha ao exportar diagnósticos: {message}',
|
'diagnostics.exportFailed': 'Falha ao exportar diagnósticos: {message}',
|
||||||
'generationPreview.title': 'Gerando…',
|
|
||||||
'generationPreview.failedTitle': 'Falha na geração',
|
|
||||||
'generationPreview.failedFallback': 'Algo deu errado. Tente novamente.',
|
|
||||||
'generationPreview.footnote': 'Normalmente leva de 2 a 5 minutos',
|
|
||||||
'generationPreview.stepUnderstand': 'Entendendo os requisitos',
|
|
||||||
'generationPreview.stepGenerate': 'Gerando a página',
|
|
||||||
'generationPreview.stepPrepare': 'Preparando a prévia',
|
|
||||||
'generationPreview.elapsed': '{elapsed} decorridos',
|
|
||||||
'generationPreview.estimate': 'Normalmente 2–5 min',
|
|
||||||
'generationPreview.progressAria': 'Progresso da geração: {percent}%',
|
|
||||||
'generationPreview.retry': 'Tentar novamente',
|
|
||||||
'generationPreview.awaitingTitle': 'Aguardando sua resposta',
|
|
||||||
'generationPreview.awaitingLead': 'Responda algumas perguntas no chat para continuar.',
|
|
||||||
'generationPreview.stoppedTitle': 'Geração pausada',
|
|
||||||
'generationPreview.stoppedLead': 'Retome as etapas restantes pelo chat à esquerda.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1856,19 +1856,4 @@ export const ru: Dict = {
|
||||||
'diagnostics.exporting': 'Экспортирование…',
|
'diagnostics.exporting': 'Экспортирование…',
|
||||||
'diagnostics.exportSuccess': 'Диагностика сохранена: {path}',
|
'diagnostics.exportSuccess': 'Диагностика сохранена: {path}',
|
||||||
'diagnostics.exportFailed': 'Не удалось экспортировать диагностику: {message}',
|
'diagnostics.exportFailed': 'Не удалось экспортировать диагностику: {message}',
|
||||||
'generationPreview.title': 'Генерация…',
|
|
||||||
'generationPreview.failedTitle': 'Ошибка генерации',
|
|
||||||
'generationPreview.failedFallback': 'Что-то пошло не так. Попробуйте ещё раз.',
|
|
||||||
'generationPreview.footnote': 'Обычно занимает 2–5 минут',
|
|
||||||
'generationPreview.stepUnderstand': 'Анализ требований',
|
|
||||||
'generationPreview.stepGenerate': 'Создание страницы',
|
|
||||||
'generationPreview.stepPrepare': 'Подготовка предпросмотра',
|
|
||||||
'generationPreview.elapsed': 'Прошло {elapsed}',
|
|
||||||
'generationPreview.estimate': 'Обычно 2–5 мин',
|
|
||||||
'generationPreview.progressAria': 'Прогресс генерации: {percent}%',
|
|
||||||
'generationPreview.retry': 'Повторить',
|
|
||||||
'generationPreview.awaitingTitle': 'Ожидание вашего ответа',
|
|
||||||
'generationPreview.awaitingLead': 'Ответьте на несколько вопросов в чате, чтобы продолжить.',
|
|
||||||
'generationPreview.stoppedTitle': 'Генерация приостановлена',
|
|
||||||
'generationPreview.stoppedLead': 'Продолжите оставшиеся шаги в чате слева.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1645,19 +1645,4 @@ export const th: Dict = {
|
||||||
'settings.designSystemsCategory': 'หมวดหมู่',
|
'settings.designSystemsCategory': 'หมวดหมู่',
|
||||||
'settings.designSystemsAllCategories': 'ทุกหมวดหมู่',
|
'settings.designSystemsAllCategories': 'ทุกหมวดหมู่',
|
||||||
'settings.designSystemsShowInHomeGallery': 'แสดงในแกลเลอรีหน้าแรก',
|
'settings.designSystemsShowInHomeGallery': 'แสดงในแกลเลอรีหน้าแรก',
|
||||||
'generationPreview.title': 'กำลังสร้าง…',
|
|
||||||
'generationPreview.failedTitle': 'การสร้างล้มเหลว',
|
|
||||||
'generationPreview.failedFallback': 'เกิดข้อผิดพลาด โปรดลองอีกครั้ง',
|
|
||||||
'generationPreview.footnote': 'โดยปกติใช้เวลา 2–5 นาที',
|
|
||||||
'generationPreview.stepUnderstand': 'กำลังทำความเข้าใจความต้องการ',
|
|
||||||
'generationPreview.stepGenerate': 'กำลังสร้างหน้า',
|
|
||||||
'generationPreview.stepPrepare': 'กำลังเตรียมตัวอย่าง',
|
|
||||||
'generationPreview.elapsed': 'ผ่านไป {elapsed}',
|
|
||||||
'generationPreview.estimate': 'โดยปกติ 2–5 นาที',
|
|
||||||
'generationPreview.progressAria': 'ความคืบหน้าการสร้าง: {percent}%',
|
|
||||||
'generationPreview.retry': 'ลองใหม่',
|
|
||||||
'generationPreview.awaitingTitle': 'กำลังรอข้อมูลจากคุณ',
|
|
||||||
'generationPreview.awaitingLead': 'ตอบคำถามสองสามข้อในแชทเพื่อดำเนินการต่อ',
|
|
||||||
'generationPreview.stoppedTitle': 'หยุดการสร้างชั่วคราว',
|
|
||||||
'generationPreview.stoppedLead': 'ดำเนินการขั้นตอนที่เหลือต่อจากแชทด้านซ้าย',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1802,19 +1802,4 @@ export const tr: Dict = {
|
||||||
'diagnostics.exporting': 'Dışa aktarılıyor…',
|
'diagnostics.exporting': 'Dışa aktarılıyor…',
|
||||||
'diagnostics.exportSuccess': 'Tanılama {path} konumuna kaydedildi',
|
'diagnostics.exportSuccess': 'Tanılama {path} konumuna kaydedildi',
|
||||||
'diagnostics.exportFailed': 'Tanılama dışa aktarılamadı: {message}',
|
'diagnostics.exportFailed': 'Tanılama dışa aktarılamadı: {message}',
|
||||||
'generationPreview.title': 'Oluşturuluyor…',
|
|
||||||
'generationPreview.failedTitle': 'Oluşturma başarısız',
|
|
||||||
'generationPreview.failedFallback': 'Bir şeyler ters gitti. Lütfen tekrar deneyin.',
|
|
||||||
'generationPreview.footnote': 'Genellikle 2–5 dakika sürer',
|
|
||||||
'generationPreview.stepUnderstand': 'Gereksinimler anlaşılıyor',
|
|
||||||
'generationPreview.stepGenerate': 'Sayfa oluşturuluyor',
|
|
||||||
'generationPreview.stepPrepare': 'Önizleme hazırlanıyor',
|
|
||||||
'generationPreview.elapsed': '{elapsed} geçti',
|
|
||||||
'generationPreview.estimate': 'Genellikle 2–5 dk',
|
|
||||||
'generationPreview.progressAria': 'Oluşturma ilerlemesi: %{percent}',
|
|
||||||
'generationPreview.retry': 'Yeniden dene',
|
|
||||||
'generationPreview.awaitingTitle': 'Yanıtınız bekleniyor',
|
|
||||||
'generationPreview.awaitingLead': 'Devam etmek için sohbette birkaç soruyu yanıtlayın.',
|
|
||||||
'generationPreview.stoppedTitle': 'Oluşturma duraklatıldı',
|
|
||||||
'generationPreview.stoppedLead': 'Kalan adımları soldaki sohbetten sürdürün.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1858,19 +1858,4 @@ export const uk: Dict = {
|
||||||
'diagnostics.exporting': 'Експортування…',
|
'diagnostics.exporting': 'Експортування…',
|
||||||
'diagnostics.exportSuccess': 'Діагностику збережено: {path}',
|
'diagnostics.exportSuccess': 'Діагностику збережено: {path}',
|
||||||
'diagnostics.exportFailed': 'Не вдалося експортувати діагностику: {message}',
|
'diagnostics.exportFailed': 'Не вдалося експортувати діагностику: {message}',
|
||||||
'generationPreview.title': 'Генерація…',
|
|
||||||
'generationPreview.failedTitle': 'Помилка генерації',
|
|
||||||
'generationPreview.failedFallback': 'Щось пішло не так. Спробуйте ще раз.',
|
|
||||||
'generationPreview.footnote': 'Зазвичай триває 2–5 хвилин',
|
|
||||||
'generationPreview.stepUnderstand': 'Аналіз вимог',
|
|
||||||
'generationPreview.stepGenerate': 'Створення сторінки',
|
|
||||||
'generationPreview.stepPrepare': 'Підготовка попереднього перегляду',
|
|
||||||
'generationPreview.elapsed': 'Минуло {elapsed}',
|
|
||||||
'generationPreview.estimate': 'Зазвичай 2–5 хв',
|
|
||||||
'generationPreview.progressAria': 'Прогрес генерації: {percent}%',
|
|
||||||
'generationPreview.retry': 'Повторити',
|
|
||||||
'generationPreview.awaitingTitle': 'Очікування вашої відповіді',
|
|
||||||
'generationPreview.awaitingLead': 'Дайте відповідь на кілька запитань у чаті, щоб продовжити.',
|
|
||||||
'generationPreview.stoppedTitle': 'Генерацію призупинено',
|
|
||||||
'generationPreview.stoppedLead': 'Продовжте решту кроків у чаті ліворуч.',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2573,19 +2573,4 @@ export const zhCN: Dict = {
|
||||||
'diagnostics.exporting': '导出中…',
|
'diagnostics.exporting': '导出中…',
|
||||||
'diagnostics.exportSuccess': '诊断日志已保存到 {path}',
|
'diagnostics.exportSuccess': '诊断日志已保存到 {path}',
|
||||||
'diagnostics.exportFailed': '导出诊断日志失败:{message}',
|
'diagnostics.exportFailed': '导出诊断日志失败:{message}',
|
||||||
'generationPreview.title': '正在生成…',
|
|
||||||
'generationPreview.failedTitle': '生成失败',
|
|
||||||
'generationPreview.failedFallback': '出现错误,请重试。',
|
|
||||||
'generationPreview.footnote': '通常需要 2–5 分钟',
|
|
||||||
'generationPreview.stepUnderstand': '理解需求',
|
|
||||||
'generationPreview.stepGenerate': '生成页面',
|
|
||||||
'generationPreview.stepPrepare': '准备预览',
|
|
||||||
'generationPreview.elapsed': '已等待 {elapsed}',
|
|
||||||
'generationPreview.estimate': '通常需要 2–5 分钟',
|
|
||||||
'generationPreview.progressAria': '生成进度:{percent}%',
|
|
||||||
'generationPreview.retry': '重试',
|
|
||||||
'generationPreview.awaitingTitle': '等待你的补充',
|
|
||||||
'generationPreview.awaitingLead': '在左侧聊天里回答几个问题即可继续生成。',
|
|
||||||
'generationPreview.stoppedTitle': '生成已暂停',
|
|
||||||
'generationPreview.stoppedLead': '可在左侧聊天里继续未完成的步骤。',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2135,19 +2135,4 @@ export const zhTW: Dict = {
|
||||||
'skillPluginCandidate.publishRepo': '發布倉庫',
|
'skillPluginCandidate.publishRepo': '發布倉庫',
|
||||||
'skillPluginCandidate.dismiss': '忽略',
|
'skillPluginCandidate.dismiss': '忽略',
|
||||||
'skillPluginCandidate.repoDescription': '這個倉庫看起來可以做成外掛。',
|
'skillPluginCandidate.repoDescription': '這個倉庫看起來可以做成外掛。',
|
||||||
'generationPreview.title': '正在生成…',
|
|
||||||
'generationPreview.failedTitle': '生成失敗',
|
|
||||||
'generationPreview.failedFallback': '發生錯誤,請重試。',
|
|
||||||
'generationPreview.footnote': '通常需要 2–5 分鐘',
|
|
||||||
'generationPreview.stepUnderstand': '理解需求',
|
|
||||||
'generationPreview.stepGenerate': '生成頁面',
|
|
||||||
'generationPreview.stepPrepare': '準備預覽',
|
|
||||||
'generationPreview.elapsed': '已等待 {elapsed}',
|
|
||||||
'generationPreview.estimate': '通常需要 2–5 分鐘',
|
|
||||||
'generationPreview.progressAria': '生成進度:{percent}%',
|
|
||||||
'generationPreview.retry': '重試',
|
|
||||||
'generationPreview.awaitingTitle': '等待你的補充',
|
|
||||||
'generationPreview.awaitingLead': '在左側聊天裡回答幾個問題即可繼續生成。',
|
|
||||||
'generationPreview.stoppedTitle': '生成已暫停',
|
|
||||||
'generationPreview.stoppedLead': '可在左側聊天裡繼續未完成的步驟。',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1914,21 +1914,6 @@ export interface Dict {
|
||||||
'workspace.openFromDesignFiles': string;
|
'workspace.openFromDesignFiles': string;
|
||||||
'workspace.designFilesLink': string;
|
'workspace.designFilesLink': string;
|
||||||
'workspace.loadingSketch': string;
|
'workspace.loadingSketch': string;
|
||||||
'generationPreview.title': string;
|
|
||||||
'generationPreview.failedTitle': string;
|
|
||||||
'generationPreview.failedFallback': string;
|
|
||||||
'generationPreview.footnote': string;
|
|
||||||
'generationPreview.stepUnderstand': string;
|
|
||||||
'generationPreview.stepGenerate': string;
|
|
||||||
'generationPreview.stepPrepare': string;
|
|
||||||
'generationPreview.elapsed': string;
|
|
||||||
'generationPreview.estimate': string;
|
|
||||||
'generationPreview.progressAria': string;
|
|
||||||
'generationPreview.retry': string;
|
|
||||||
'generationPreview.awaitingTitle': string;
|
|
||||||
'generationPreview.awaitingLead': string;
|
|
||||||
'generationPreview.stoppedTitle': string;
|
|
||||||
'generationPreview.stoppedLead': string;
|
|
||||||
'designFiles.title': string;
|
'designFiles.title': string;
|
||||||
'designFiles.upload': string;
|
'designFiles.upload': string;
|
||||||
'designFiles.pasteText': string;
|
'designFiles.pasteText': string;
|
||||||
|
|
|
||||||
|
|
@ -1,335 +0,0 @@
|
||||||
import type { AgentEvent, ChatMessage, LiveArtifactSummary, ProjectFile } from '../types';
|
|
||||||
import { isLiveArtifactTabId, liveArtifactTabId } from '../types';
|
|
||||||
import { isTodoWriteToolName, latestTodosFromEvents, type TodoItem } from './todos';
|
|
||||||
|
|
||||||
export type GenerationStepStatus = 'pending' | 'running' | 'succeeded' | 'failed';
|
|
||||||
|
|
||||||
export type GenerationPhase = 'generating' | 'awaiting-input' | 'stopped' | 'failed';
|
|
||||||
|
|
||||||
export interface GenerationPreviewStep {
|
|
||||||
id: 'understand' | 'generate' | 'prepare';
|
|
||||||
status: GenerationStepStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GenerationPreviewModel {
|
|
||||||
startedAt: number;
|
|
||||||
steps: GenerationPreviewStep[];
|
|
||||||
phase: GenerationPhase;
|
|
||||||
failed: boolean;
|
|
||||||
errorMessage: string | null;
|
|
||||||
progressPercent: number;
|
|
||||||
/**
|
|
||||||
* Latest human-readable activity snippet pulled from the streamed
|
|
||||||
* events. Only set while actively generating so the waiting surface
|
|
||||||
* shows real movement instead of a frozen card.
|
|
||||||
*/
|
|
||||||
activityLabel: string | null;
|
|
||||||
/**
|
|
||||||
* Concrete sub-status for the long "generating" phase, e.g. the
|
|
||||||
* in-progress task ("Writing index.html") or the current write target.
|
|
||||||
* Lets the middle step show movement without splitting into more
|
|
||||||
* (less reliable) discrete steps. Only set while generating.
|
|
||||||
*/
|
|
||||||
detailLabel: string | null;
|
|
||||||
/**
|
|
||||||
* Task counter derived from the agent's TodoWrite plan, e.g. 3/8. The
|
|
||||||
* in-progress task counts toward `done` to match the chat-side todo card.
|
|
||||||
* Only set while generating and when the agent emitted a plan.
|
|
||||||
*/
|
|
||||||
todoProgress: { done: number; total: number } | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matches the inline forms the agent emits to ask the user clarifying
|
|
||||||
// questions before continuing (see artifacts/question-form.ts).
|
|
||||||
const QUESTION_FORM_RE = /<(question-form|ask-question)\b/i;
|
|
||||||
|
|
||||||
// Tools that represent concrete generation work (writing/editing files,
|
|
||||||
// running commands) as opposed to reads/plans.
|
|
||||||
const WRITE_LIKE_TOOL_RE = /^(write|edit|multiedit|bash|run_terminal_cmd)$/i;
|
|
||||||
|
|
||||||
const PREVIEWABLE_FILE = /\.(html?|jsx|tsx|svg|md|pdf|pptx?|key)$/i;
|
|
||||||
|
|
||||||
export function workspaceHasPreviewSurface(input: {
|
|
||||||
activeTab: string | null;
|
|
||||||
projectFiles: ProjectFile[];
|
|
||||||
liveArtifacts: LiveArtifactSummary[];
|
|
||||||
streamingArtifactHtml?: string | null | undefined;
|
|
||||||
}): boolean {
|
|
||||||
if (input.streamingArtifactHtml?.trim()) return true;
|
|
||||||
const active = input.activeTab;
|
|
||||||
if (!active) return false;
|
|
||||||
if (isLiveArtifactTabId(active)) {
|
|
||||||
return input.liveArtifacts.some((entry) => liveArtifactTabId(entry.id) === active);
|
|
||||||
}
|
|
||||||
const file = input.projectFiles.find((item) => item.name === active);
|
|
||||||
if (!file) return false;
|
|
||||||
if (file.kind === 'image' || file.kind === 'video' || file.kind === 'audio' || file.kind === 'sketch') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (PREVIEWABLE_FILE.test(file.name)) return true;
|
|
||||||
return file.kind === 'html' || file.kind === 'code' || file.kind === 'text';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deriveGenerationPreviewModel(input: {
|
|
||||||
events: AgentEvent[];
|
|
||||||
hasArtifactHtml: boolean;
|
|
||||||
hasPreviewSurface: boolean;
|
|
||||||
failed: boolean;
|
|
||||||
errorMessage?: string | null;
|
|
||||||
}): Pick<GenerationPreviewModel, 'steps' | 'progressPercent' | 'errorMessage'> {
|
|
||||||
const steps = derivePrototypeGenerationSteps({
|
|
||||||
events: input.events,
|
|
||||||
hasArtifactHtml: input.hasArtifactHtml,
|
|
||||||
hasPreviewSurface: input.hasPreviewSurface,
|
|
||||||
failed: input.failed,
|
|
||||||
});
|
|
||||||
const progressPercent = generationPreviewProgress(steps);
|
|
||||||
return {
|
|
||||||
steps,
|
|
||||||
progressPercent,
|
|
||||||
errorMessage: input.failed ? input.errorMessage?.trim() || failureMessageFromEvents(input.events) : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildGenerationPreviewState(input: {
|
|
||||||
designSystemProject: boolean;
|
|
||||||
messages: ChatMessage[];
|
|
||||||
streaming: boolean;
|
|
||||||
activeTab: string | null;
|
|
||||||
projectFiles: ProjectFile[];
|
|
||||||
liveArtifacts: LiveArtifactSummary[];
|
|
||||||
artifactHtml?: string | null;
|
|
||||||
conversationError?: string | null;
|
|
||||||
}): (GenerationPreviewModel & { retryTarget: ChatMessage | null }) | null {
|
|
||||||
if (input.designSystemProject) return null;
|
|
||||||
|
|
||||||
const hasPreviewSurface = workspaceHasPreviewSurface({
|
|
||||||
activeTab: input.activeTab,
|
|
||||||
projectFiles: input.projectFiles,
|
|
||||||
liveArtifacts: input.liveArtifacts,
|
|
||||||
streamingArtifactHtml: input.artifactHtml,
|
|
||||||
});
|
|
||||||
|
|
||||||
const latestAssistant = [...input.messages]
|
|
||||||
.reverse()
|
|
||||||
.find((message) => message.role === 'assistant');
|
|
||||||
|
|
||||||
if (!latestAssistant) return null;
|
|
||||||
|
|
||||||
const status = latestAssistant.runStatus;
|
|
||||||
const runActive = isActiveRunStatus(status) || input.streaming;
|
|
||||||
const runFailed = status === 'failed';
|
|
||||||
const runStopped = status === 'canceled';
|
|
||||||
// The agent finished its turn but is waiting on the user to answer an
|
|
||||||
// inline question form before it can keep going.
|
|
||||||
const awaitingInput =
|
|
||||||
!runActive && !runFailed && !runStopped && messageHasPendingQuestion(latestAssistant);
|
|
||||||
|
|
||||||
let phase: GenerationPhase;
|
|
||||||
if (runFailed) {
|
|
||||||
phase = 'failed';
|
|
||||||
} else if (runActive) {
|
|
||||||
phase = 'generating';
|
|
||||||
} else if (runStopped) {
|
|
||||||
phase = 'stopped';
|
|
||||||
} else if (awaitingInput) {
|
|
||||||
phase = 'awaiting-input';
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once the user has something previewable, only the error state takes
|
|
||||||
// over the surface; the calmer waiting states defer to the live preview
|
|
||||||
// so we never hide a finished artifact behind a status card.
|
|
||||||
if (hasPreviewSurface && phase !== 'failed') return null;
|
|
||||||
|
|
||||||
const failed = phase === 'failed';
|
|
||||||
const events = latestAssistant.events ?? [];
|
|
||||||
const derived = deriveGenerationPreviewModel({
|
|
||||||
events,
|
|
||||||
hasArtifactHtml: Boolean(input.artifactHtml?.trim()),
|
|
||||||
hasPreviewSurface,
|
|
||||||
failed,
|
|
||||||
errorMessage: input.conversationError,
|
|
||||||
});
|
|
||||||
|
|
||||||
const startedAt = latestAssistant.startedAt ?? latestAssistant.createdAt ?? Date.now();
|
|
||||||
|
|
||||||
const generating = phase === 'generating';
|
|
||||||
const todos = generating ? latestTodosFromEvents(events) : [];
|
|
||||||
const todoProgress =
|
|
||||||
todos.length > 0
|
|
||||||
? {
|
|
||||||
done: todos.filter(
|
|
||||||
(todo) => todo.status === 'completed' || todo.status === 'in_progress',
|
|
||||||
).length,
|
|
||||||
total: todos.length,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
startedAt,
|
|
||||||
steps: derived.steps,
|
|
||||||
phase,
|
|
||||||
failed,
|
|
||||||
errorMessage: derived.errorMessage,
|
|
||||||
progressPercent: derived.progressPercent,
|
|
||||||
activityLabel: generating ? latestActivityLabel(events) : null,
|
|
||||||
detailLabel: generating ? generationDetailLabel(events, todos) : null,
|
|
||||||
todoProgress,
|
|
||||||
retryTarget: failed ? latestAssistant : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function derivePrototypeGenerationSteps(input: {
|
|
||||||
events: AgentEvent[];
|
|
||||||
hasArtifactHtml: boolean;
|
|
||||||
hasPreviewSurface: boolean;
|
|
||||||
failed: boolean;
|
|
||||||
}): GenerationPreviewStep[] {
|
|
||||||
const hasStatus = (labels: string[]) =>
|
|
||||||
eventsHaveStatus(input.events, labels);
|
|
||||||
const hasToolUse = input.events.some((event) => event.kind === 'tool_use');
|
|
||||||
const hasWriteLikeTool = input.events.some(
|
|
||||||
(event) =>
|
|
||||||
event.kind === 'tool_use'
|
|
||||||
&& typeof event.name === 'string'
|
|
||||||
&& /^(write|edit|bash|run_terminal_cmd)$/i.test(event.name),
|
|
||||||
);
|
|
||||||
const hasArtifactStart = input.events.some(
|
|
||||||
(event) => event.kind === 'text' && event.text.includes('<artifact'),
|
|
||||||
) || input.hasArtifactHtml;
|
|
||||||
const hasText = input.events.some((event) => event.kind === 'text' && event.text.trim().length > 0);
|
|
||||||
|
|
||||||
let understand: GenerationStepStatus = 'running';
|
|
||||||
if (input.failed && !hasText && !hasToolUse) {
|
|
||||||
understand = 'failed';
|
|
||||||
} else if (hasText || hasStatus(['thinking', 'streaming']) || hasToolUse) {
|
|
||||||
// `requesting`/`starting` only mean the request left the client — the
|
|
||||||
// model hasn't produced anything yet, so we keep "understand" in
|
|
||||||
// progress until real thinking/output/tool activity arrives. This lets
|
|
||||||
// the UI reveal the steps one at a time instead of jumping straight to
|
|
||||||
// a fully populated row.
|
|
||||||
understand = 'succeeded';
|
|
||||||
}
|
|
||||||
|
|
||||||
let generate: GenerationStepStatus = 'pending';
|
|
||||||
if (understand === 'succeeded') {
|
|
||||||
generate = 'running';
|
|
||||||
}
|
|
||||||
if (hasWriteLikeTool || hasArtifactStart) {
|
|
||||||
generate = 'succeeded';
|
|
||||||
}
|
|
||||||
if (input.failed && understand === 'succeeded' && !hasWriteLikeTool && !hasArtifactStart) {
|
|
||||||
generate = 'failed';
|
|
||||||
}
|
|
||||||
|
|
||||||
let prepare: GenerationStepStatus = 'pending';
|
|
||||||
if (generate === 'succeeded') {
|
|
||||||
prepare = 'running';
|
|
||||||
}
|
|
||||||
if (input.hasPreviewSurface || input.hasArtifactHtml) {
|
|
||||||
prepare = 'succeeded';
|
|
||||||
}
|
|
||||||
if (input.failed && generate === 'succeeded' && !input.hasPreviewSurface && !input.hasArtifactHtml) {
|
|
||||||
prepare = 'failed';
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{ id: 'understand', status: understand },
|
|
||||||
{ id: 'generate', status: generate },
|
|
||||||
{ id: 'prepare', status: prepare },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generationPreviewProgress(steps: GenerationPreviewStep[]): number {
|
|
||||||
if (steps.length === 0) return 8;
|
|
||||||
const weights = { pending: 0, running: 0.45, succeeded: 1, failed: 0.2 };
|
|
||||||
const score = steps.reduce((sum, step) => sum + weights[step.status], 0) / steps.length;
|
|
||||||
return Math.max(8, Math.min(steps.some((step) => step.status === 'failed') ? 72 : 92, Math.round(score * 100)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function isActiveRunStatus(status: ChatMessage['runStatus']): boolean {
|
|
||||||
return status === 'queued' || status === 'running';
|
|
||||||
}
|
|
||||||
|
|
||||||
function messageHasPendingQuestion(message: ChatMessage): boolean {
|
|
||||||
if (typeof message.content === 'string' && QUESTION_FORM_RE.test(message.content)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const events = message.events ?? [];
|
|
||||||
return events.some((event) => event.kind === 'text' && QUESTION_FORM_RE.test(event.text));
|
|
||||||
}
|
|
||||||
|
|
||||||
function latestActivityLabel(events: AgentEvent[]): string | null {
|
|
||||||
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
||||||
const event = events[index]!;
|
|
||||||
if (event.kind === 'thinking' && event.text.trim()) {
|
|
||||||
return truncateActivity(event.text);
|
|
||||||
}
|
|
||||||
if (event.kind === 'text' && event.text.trim() && !QUESTION_FORM_RE.test(event.text)) {
|
|
||||||
return truncateActivity(event.text);
|
|
||||||
}
|
|
||||||
// Intentionally skip `status` details: their payload is often an
|
|
||||||
// internal identifier (e.g. the model slug from a `requesting` event)
|
|
||||||
// rather than human-readable progress, so surfacing it reads as noise.
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function truncateActivity(text: string): string {
|
|
||||||
const collapsed = text.replace(/\s+/g, ' ').trim();
|
|
||||||
return collapsed.length > 80 ? `${collapsed.slice(0, 79)}…` : collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The concrete operation behind the "generating" step. Prefers the agent's
|
|
||||||
// own in-progress task label (TodoWrite `activeForm`/content), then falls
|
|
||||||
// back to the most recent write/edit target file so the middle phase still
|
|
||||||
// shows movement when no plan was emitted.
|
|
||||||
function generationDetailLabel(events: AgentEvent[], todos: TodoItem[]): string | null {
|
|
||||||
const active = todos.find((todo) => todo.status === 'in_progress');
|
|
||||||
if (active) {
|
|
||||||
const label = active.activeForm?.trim() || active.content.trim();
|
|
||||||
if (label) return truncateActivity(label);
|
|
||||||
}
|
|
||||||
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
||||||
const event = events[index]!;
|
|
||||||
if (
|
|
||||||
event.kind === 'tool_use'
|
|
||||||
&& typeof event.name === 'string'
|
|
||||||
&& !isTodoWriteToolName(event.name)
|
|
||||||
&& WRITE_LIKE_TOOL_RE.test(event.name)
|
|
||||||
) {
|
|
||||||
const target = toolTargetName(event.input);
|
|
||||||
if (target) return target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toolTargetName(input: unknown): string | null {
|
|
||||||
if (!input || typeof input !== 'object') return null;
|
|
||||||
const obj = input as Record<string, unknown>;
|
|
||||||
const raw = obj.file_path ?? obj.filePath ?? obj.path ?? obj.file;
|
|
||||||
if (typeof raw !== 'string' || !raw.trim()) return null;
|
|
||||||
const segments = raw.trim().split(/[\\/]/);
|
|
||||||
return segments[segments.length - 1] || raw.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function eventsHaveStatus(events: AgentEvent[], labels: string[]): boolean {
|
|
||||||
const normalized = new Set(labels.map((label) => label.toLowerCase()));
|
|
||||||
return events.some(
|
|
||||||
(event) =>
|
|
||||||
event.kind === 'status'
|
|
||||||
&& normalized.has(event.label.toLowerCase()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function failureMessageFromEvents(events: AgentEvent[]): string | null {
|
|
||||||
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
||||||
const event = events[index]!;
|
|
||||||
if (event.kind === 'text' && event.text.trim()) return event.text.trim();
|
|
||||||
if (event.kind === 'status' && event.detail?.trim()) return event.detail.trim();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
@ -1,311 +0,0 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import {
|
|
||||||
buildGenerationPreviewState,
|
|
||||||
derivePrototypeGenerationSteps,
|
|
||||||
workspaceHasPreviewSurface,
|
|
||||||
} from '../../src/runtime/generation-preview';
|
|
||||||
import type { AgentEvent, ChatMessage } from '../../src/types';
|
|
||||||
|
|
||||||
describe('generation preview helpers', () => {
|
|
||||||
it('detects when the workspace already has a preview surface', () => {
|
|
||||||
expect(
|
|
||||||
workspaceHasPreviewSurface({
|
|
||||||
activeTab: 'index.html',
|
|
||||||
projectFiles: [{ name: 'index.html', size: 1, mtime: 1, kind: 'html', mime: 'text/html' }],
|
|
||||||
liveArtifacts: [],
|
|
||||||
}),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
workspaceHasPreviewSurface({
|
|
||||||
activeTab: null,
|
|
||||||
projectFiles: [],
|
|
||||||
liveArtifacts: [],
|
|
||||||
streamingArtifactHtml: '<html><body>hi</body></html>',
|
|
||||||
}),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('advances the three prototype steps from streamed events', () => {
|
|
||||||
const events: AgentEvent[] = [
|
|
||||||
{ kind: 'status', label: 'thinking' },
|
|
||||||
{ kind: 'text', text: 'Planning the page.' },
|
|
||||||
{ kind: 'tool_use', id: '1', name: 'Write', input: { file_path: 'index.html' } },
|
|
||||||
];
|
|
||||||
expect(
|
|
||||||
derivePrototypeGenerationSteps({
|
|
||||||
events,
|
|
||||||
hasArtifactHtml: false,
|
|
||||||
hasPreviewSurface: false,
|
|
||||||
failed: false,
|
|
||||||
}),
|
|
||||||
).toEqual([
|
|
||||||
{ id: 'understand', status: 'succeeded' },
|
|
||||||
{ id: 'generate', status: 'succeeded' },
|
|
||||||
{ id: 'prepare', status: 'running' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps the understand step in progress while the request is still pending', () => {
|
|
||||||
// `requesting` only means the request left the client; nothing should
|
|
||||||
// advance past the first step until real model activity arrives, so the
|
|
||||||
// UI can reveal steps one at a time.
|
|
||||||
expect(
|
|
||||||
derivePrototypeGenerationSteps({
|
|
||||||
events: [{ kind: 'status', label: 'requesting', detail: 'claude-opus-4-7' }],
|
|
||||||
hasArtifactHtml: false,
|
|
||||||
hasPreviewSurface: false,
|
|
||||||
failed: false,
|
|
||||||
}),
|
|
||||||
).toEqual([
|
|
||||||
{ id: 'understand', status: 'running' },
|
|
||||||
{ id: 'generate', status: 'pending' },
|
|
||||||
{ id: 'prepare', status: 'pending' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('builds preview state for an active assistant run without an open preview tab', () => {
|
|
||||||
const assistant: ChatMessage = {
|
|
||||||
id: 'a1',
|
|
||||||
role: 'assistant',
|
|
||||||
content: '',
|
|
||||||
runStatus: 'running',
|
|
||||||
startedAt: Date.now() - 5_000,
|
|
||||||
events: [{ kind: 'status', label: 'thinking' }],
|
|
||||||
};
|
|
||||||
const state = buildGenerationPreviewState({
|
|
||||||
designSystemProject: false,
|
|
||||||
messages: [{ id: 'u1', role: 'user', content: 'Build a landing page' }, assistant],
|
|
||||||
streaming: true,
|
|
||||||
activeTab: null,
|
|
||||||
projectFiles: [],
|
|
||||||
liveArtifacts: [],
|
|
||||||
});
|
|
||||||
expect(state).not.toBeNull();
|
|
||||||
expect(state?.phase).toBe('generating');
|
|
||||||
// A `thinking` status is enough evidence that the model started, so the
|
|
||||||
// first step has already advanced past "running".
|
|
||||||
expect(state?.steps[0]?.status).toBe('succeeded');
|
|
||||||
expect(state?.retryTarget).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('surfaces the latest activity snippet while generating', () => {
|
|
||||||
const assistant: ChatMessage = {
|
|
||||||
id: 'a1',
|
|
||||||
role: 'assistant',
|
|
||||||
content: '',
|
|
||||||
runStatus: 'running',
|
|
||||||
startedAt: Date.now(),
|
|
||||||
events: [
|
|
||||||
{ kind: 'status', label: 'thinking' },
|
|
||||||
{ kind: 'thinking', text: 'Sketching the hero section layout' },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const state = buildGenerationPreviewState({
|
|
||||||
designSystemProject: false,
|
|
||||||
messages: [assistant],
|
|
||||||
streaming: true,
|
|
||||||
activeTab: null,
|
|
||||||
projectFiles: [],
|
|
||||||
liveArtifacts: [],
|
|
||||||
});
|
|
||||||
expect(state?.activityLabel).toBe('Sketching the hero section layout');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('derives a concrete sub-status and task count while generating', () => {
|
|
||||||
const assistant: ChatMessage = {
|
|
||||||
id: 'a1',
|
|
||||||
role: 'assistant',
|
|
||||||
content: '',
|
|
||||||
runStatus: 'running',
|
|
||||||
startedAt: Date.now(),
|
|
||||||
events: [
|
|
||||||
{
|
|
||||||
kind: 'tool_use',
|
|
||||||
id: 't1',
|
|
||||||
name: 'TodoWrite',
|
|
||||||
input: {
|
|
||||||
todos: [
|
|
||||||
{ content: 'Plan layout', status: 'completed' },
|
|
||||||
{ content: 'Write index.html', activeForm: 'Writing index.html', status: 'in_progress' },
|
|
||||||
{ content: 'Self-check', status: 'pending' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const state = buildGenerationPreviewState({
|
|
||||||
designSystemProject: false,
|
|
||||||
messages: [assistant],
|
|
||||||
streaming: true,
|
|
||||||
activeTab: null,
|
|
||||||
projectFiles: [],
|
|
||||||
liveArtifacts: [],
|
|
||||||
});
|
|
||||||
expect(state?.detailLabel).toBe('Writing index.html');
|
|
||||||
// The in-progress task counts toward `done`, matching the chat todo card.
|
|
||||||
expect(state?.todoProgress).toEqual({ done: 2, total: 3 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('falls back to the latest write target when no plan is present', () => {
|
|
||||||
const assistant: ChatMessage = {
|
|
||||||
id: 'a1',
|
|
||||||
role: 'assistant',
|
|
||||||
content: '',
|
|
||||||
runStatus: 'running',
|
|
||||||
startedAt: Date.now(),
|
|
||||||
events: [
|
|
||||||
{ kind: 'text', text: 'Writing the page now.' },
|
|
||||||
{ kind: 'tool_use', id: 't1', name: 'Write', input: { file_path: 'src/index.html' } },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const state = buildGenerationPreviewState({
|
|
||||||
designSystemProject: false,
|
|
||||||
messages: [assistant],
|
|
||||||
streaming: true,
|
|
||||||
activeTab: null,
|
|
||||||
projectFiles: [],
|
|
||||||
liveArtifacts: [],
|
|
||||||
});
|
|
||||||
expect(state?.detailLabel).toBe('index.html');
|
|
||||||
expect(state?.todoProgress).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('omits sub-status data once the run is no longer generating', () => {
|
|
||||||
const assistant: ChatMessage = {
|
|
||||||
id: 'a1',
|
|
||||||
role: 'assistant',
|
|
||||||
content: 'Partial work',
|
|
||||||
runStatus: 'canceled',
|
|
||||||
startedAt: Date.now(),
|
|
||||||
events: [
|
|
||||||
{
|
|
||||||
kind: 'tool_use',
|
|
||||||
id: 't1',
|
|
||||||
name: 'TodoWrite',
|
|
||||||
input: { todos: [{ content: 'Write index.html', status: 'in_progress' }] },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const state = buildGenerationPreviewState({
|
|
||||||
designSystemProject: false,
|
|
||||||
messages: [assistant],
|
|
||||||
streaming: false,
|
|
||||||
activeTab: null,
|
|
||||||
projectFiles: [],
|
|
||||||
liveArtifacts: [],
|
|
||||||
});
|
|
||||||
expect(state?.phase).toBe('stopped');
|
|
||||||
expect(state?.detailLabel).toBeNull();
|
|
||||||
expect(state?.todoProgress).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps a paused surface when the run was stopped without a preview', () => {
|
|
||||||
const assistant: ChatMessage = {
|
|
||||||
id: 'a1',
|
|
||||||
role: 'assistant',
|
|
||||||
content: 'Partial work',
|
|
||||||
runStatus: 'canceled',
|
|
||||||
startedAt: Date.now() - 10_000,
|
|
||||||
events: [{ kind: 'tool_use', id: '1', name: 'Write', input: {} }],
|
|
||||||
};
|
|
||||||
const state = buildGenerationPreviewState({
|
|
||||||
designSystemProject: false,
|
|
||||||
messages: [assistant],
|
|
||||||
streaming: false,
|
|
||||||
activeTab: null,
|
|
||||||
projectFiles: [],
|
|
||||||
liveArtifacts: [],
|
|
||||||
});
|
|
||||||
expect(state?.phase).toBe('stopped');
|
|
||||||
expect(state?.failed).toBe(false);
|
|
||||||
expect(state?.retryTarget).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps a waiting surface when the agent is asking the user a question', () => {
|
|
||||||
const assistant: ChatMessage = {
|
|
||||||
id: 'a1',
|
|
||||||
role: 'assistant',
|
|
||||||
content: 'A few quick questions:\n<question-form id="discovery" title="Brief">{"questions":[]}</question-form>',
|
|
||||||
runStatus: 'succeeded',
|
|
||||||
startedAt: Date.now() - 4_000,
|
|
||||||
events: [{ kind: 'text', text: '<question-form id="discovery">{"questions":[]}</question-form>' }],
|
|
||||||
};
|
|
||||||
const state = buildGenerationPreviewState({
|
|
||||||
designSystemProject: false,
|
|
||||||
messages: [assistant],
|
|
||||||
streaming: false,
|
|
||||||
activeTab: null,
|
|
||||||
projectFiles: [],
|
|
||||||
liveArtifacts: [],
|
|
||||||
});
|
|
||||||
expect(state?.phase).toBe('awaiting-input');
|
|
||||||
expect(state?.retryTarget).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns null for a finished run that produced no question or preview', () => {
|
|
||||||
const assistant: ChatMessage = {
|
|
||||||
id: 'a1',
|
|
||||||
role: 'assistant',
|
|
||||||
content: 'All done!',
|
|
||||||
runStatus: 'succeeded',
|
|
||||||
startedAt: Date.now() - 4_000,
|
|
||||||
events: [{ kind: 'text', text: 'All done!' }],
|
|
||||||
};
|
|
||||||
expect(
|
|
||||||
buildGenerationPreviewState({
|
|
||||||
designSystemProject: false,
|
|
||||||
messages: [assistant],
|
|
||||||
streaming: false,
|
|
||||||
activeTab: null,
|
|
||||||
projectFiles: [],
|
|
||||||
liveArtifacts: [],
|
|
||||||
}),
|
|
||||||
).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('builds a failed state with a retry target', () => {
|
|
||||||
const assistant: ChatMessage = {
|
|
||||||
id: 'a1',
|
|
||||||
role: 'assistant',
|
|
||||||
content: '',
|
|
||||||
runStatus: 'failed',
|
|
||||||
startedAt: Date.now() - 8_000,
|
|
||||||
events: [{ kind: 'text', text: 'Model request failed' }],
|
|
||||||
};
|
|
||||||
const state = buildGenerationPreviewState({
|
|
||||||
designSystemProject: false,
|
|
||||||
messages: [assistant],
|
|
||||||
streaming: false,
|
|
||||||
activeTab: null,
|
|
||||||
projectFiles: [],
|
|
||||||
liveArtifacts: [],
|
|
||||||
conversationError: 'Network error',
|
|
||||||
});
|
|
||||||
expect(state?.phase).toBe('failed');
|
|
||||||
expect(state?.failed).toBe(true);
|
|
||||||
expect(state?.errorMessage).toBe('Network error');
|
|
||||||
expect(state?.retryTarget).toBe(assistant);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hides preview state once a preview tab is active', () => {
|
|
||||||
const assistant: ChatMessage = {
|
|
||||||
id: 'a1',
|
|
||||||
role: 'assistant',
|
|
||||||
content: '',
|
|
||||||
runStatus: 'running',
|
|
||||||
startedAt: Date.now(),
|
|
||||||
events: [{ kind: 'tool_use', id: '1', name: 'Write', input: {} }],
|
|
||||||
};
|
|
||||||
expect(
|
|
||||||
buildGenerationPreviewState({
|
|
||||||
designSystemProject: false,
|
|
||||||
messages: [assistant],
|
|
||||||
streaming: true,
|
|
||||||
activeTab: 'index.html',
|
|
||||||
projectFiles: [{ name: 'index.html', size: 1, mtime: 1, kind: 'html', mime: 'text/html' }],
|
|
||||||
liveArtifacts: [],
|
|
||||||
}),
|
|
||||||
).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
Loading…
Reference in a new issue