172 lines
6.9 KiB
TypeScript
172 lines
6.9 KiB
TypeScript
import React from 'react';
|
|
import { Card, Collapse, Descriptions, Empty, Space, Table, Tag, Typography } from 'antd';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
import { JsonBlock } from './JsonCollapse';
|
|
import type {
|
|
VideoPromptSchemaFieldConfig,
|
|
VideoPromptSchemaSectionConfig,
|
|
} from '../../../utils/videoPromptSchema';
|
|
import {
|
|
hasSchemaConfigSnapshot,
|
|
inferSchemaSections,
|
|
normalizeSchemaSections,
|
|
stringifySchemaValue,
|
|
} from '../../../utils/videoPromptSchema';
|
|
|
|
const isRecord = (value: unknown): value is Record<string, unknown> => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
const isLongText = (value: unknown): boolean => typeof value === 'string' && value.length > 80;
|
|
|
|
const renderValue = (value: unknown): React.ReactNode => {
|
|
if (value === null || value === undefined || value === '') return <Typography.Text type="secondary">-</Typography.Text>;
|
|
if (typeof value === 'boolean') return value ? '是' : '否';
|
|
if (typeof value === 'number') return value;
|
|
if (typeof value === 'string') {
|
|
return <Typography.Paragraph style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>{value}</Typography.Paragraph>;
|
|
}
|
|
return <JsonBlock value={value} maxHeight={220} />;
|
|
};
|
|
|
|
const tagOfEditable = (editable?: boolean) => editable ? <Tag color="processing">可编辑</Tag> : <Tag color="default">只读</Tag>;
|
|
|
|
const readSectionValue = (schema: Record<string, any>, key: string): unknown => schema[key];
|
|
|
|
const renderObjectSection = (schema: Record<string, any>, section: VideoPromptSchemaSectionConfig): React.ReactNode => {
|
|
const value = readSectionValue(schema, section.key);
|
|
if (!isRecord(value)) return <JsonBlock value={value ?? {}} maxHeight={220} />;
|
|
const fields = section.children.filter((field) => field.enabled);
|
|
if (!fields.length) return <JsonBlock value={value} maxHeight={220} />;
|
|
|
|
return (
|
|
<Descriptions size="small" column={2} bordered>
|
|
{fields.map((field) => (
|
|
<Descriptions.Item key={field.key} label={<Space size={4}>{field.label}{tagOfEditable(field.editable)}</Space>} span={isLongText(value[field.key]) || isRecord(value[field.key]) || Array.isArray(value[field.key]) ? 2 : 1}>
|
|
{renderValue(value[field.key])}
|
|
</Descriptions.Item>
|
|
))}
|
|
</Descriptions>
|
|
);
|
|
};
|
|
|
|
const buildFlowFields = (section: VideoPromptSchemaSectionConfig, rows: Record<string, unknown>[]): VideoPromptSchemaFieldConfig[] => {
|
|
const configured = section.itemFields.filter((field) => field.enabled);
|
|
if (configured.length) {
|
|
return [{ key: '时间段', label: '时间段', enabled: true, editable: false }, ...configured];
|
|
}
|
|
const inferred = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
|
|
return inferred.map((key) => ({ key, label: key, enabled: true, editable: false }));
|
|
};
|
|
|
|
const renderFlowSection = (schema: Record<string, any>, section: VideoPromptSchemaSectionConfig): React.ReactNode => {
|
|
const value = readSectionValue(schema, section.key);
|
|
if (!Array.isArray(value) || !value.every(isRecord)) return <JsonBlock value={value ?? []} maxHeight={260} />;
|
|
const rows = value as Record<string, unknown>[];
|
|
const fields = buildFlowFields(section, rows);
|
|
const columns: ColumnsType<Record<string, unknown>> = fields.map((field) => ({
|
|
title: <Space size={4}>{field.label}{tagOfEditable(field.editable)}</Space>,
|
|
dataIndex: field.key,
|
|
key: field.key,
|
|
width: isLongText(rows.find((row) => row[field.key])?.[field.key]) ? 320 : 160,
|
|
render: (cell: unknown) => renderValue(cell),
|
|
}));
|
|
|
|
return (
|
|
<Table
|
|
size="small"
|
|
rowKey={(_, index) => `${section.key}-${index}`}
|
|
columns={columns}
|
|
dataSource={rows}
|
|
pagination={false}
|
|
scroll={{ x: Math.max(900, fields.length * 180) }}
|
|
tableLayout="fixed"
|
|
/>
|
|
);
|
|
};
|
|
|
|
const renderPrimitiveSection = (schema: Record<string, any>, section: VideoPromptSchemaSectionConfig): React.ReactNode => {
|
|
const value = readSectionValue(schema, section.key);
|
|
return renderValue(value);
|
|
};
|
|
|
|
interface VideoPromptSchemaViewerProps {
|
|
schema?: Record<string, any> | null;
|
|
finalPrompt?: string | null;
|
|
schemaConfigSnapshot?: Record<string, any> | null;
|
|
schemaConfigSource?: string | null;
|
|
schemaConfigVersion?: string | null;
|
|
schemaConfigIsFallback?: boolean | null;
|
|
}
|
|
|
|
const VideoPromptSchemaViewer: React.FC<VideoPromptSchemaViewerProps> = ({
|
|
schema,
|
|
finalPrompt,
|
|
schemaConfigSnapshot,
|
|
schemaConfigSource,
|
|
schemaConfigVersion,
|
|
schemaConfigIsFallback,
|
|
}) => {
|
|
const hasSchema = !!schema && Object.keys(schema).length > 0;
|
|
if (!hasSchema && !finalPrompt) {
|
|
return <Empty description="暂无视频提词 schema" />;
|
|
}
|
|
|
|
const safeSchema = hasSchema ? schema as Record<string, any> : {};
|
|
const hasSnapshot = hasSchemaConfigSnapshot(schemaConfigSnapshot);
|
|
const sections = hasSnapshot ? normalizeSchemaSections(schemaConfigSnapshot) : inferSchemaSections(safeSchema);
|
|
const schemaItems = sections
|
|
.filter((section) => section.key in safeSchema)
|
|
.map((section) => ({
|
|
key: section.key,
|
|
label: (
|
|
<Space size={8} wrap>
|
|
<span>{section.label}</span>
|
|
{tagOfEditable(section.editable)}
|
|
{!hasSnapshot ? <Tag color="warning">管理后台兜底展示</Tag> : null}
|
|
</Space>
|
|
),
|
|
children: section.type === 'object'
|
|
? renderObjectSection(safeSchema, section)
|
|
: section.type === 'flow'
|
|
? renderFlowSection(safeSchema, section)
|
|
: renderPrimitiveSection(safeSchema, section),
|
|
}));
|
|
|
|
return (
|
|
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
|
<Card size="small" title="Schema 配置来源">
|
|
<Descriptions size="small" column={3} bordered>
|
|
<Descriptions.Item label="来源">{schemaConfigSource || schemaConfigSnapshot?.source || (hasSnapshot ? '-' : 'viewer_fallback')}</Descriptions.Item>
|
|
<Descriptions.Item label="版本">{schemaConfigVersion || schemaConfigSnapshot?.version || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="历史兜底">{schemaConfigIsFallback || !hasSnapshot ? <Tag color="warning">是</Tag> : <Tag color="success">否</Tag>}</Descriptions.Item>
|
|
</Descriptions>
|
|
</Card>
|
|
|
|
{finalPrompt ? (
|
|
<Card size="small" title="最终视频提示词">
|
|
<Typography.Paragraph style={{ whiteSpace: 'pre-wrap', marginBottom: 0 }}>{finalPrompt}</Typography.Paragraph>
|
|
</Card>
|
|
) : null}
|
|
|
|
{schemaItems.length ? (
|
|
<Collapse size="small" items={schemaItems} />
|
|
) : hasSchema ? (
|
|
<Card size="small" title="视频提词 Schema">
|
|
<JsonBlock value={safeSchema} maxHeight={360} />
|
|
</Card>
|
|
) : null}
|
|
|
|
{hasSchema ? (
|
|
<Collapse
|
|
size="small"
|
|
items={[{
|
|
key: 'raw_schema',
|
|
label: '原始 JSON / 历史脏字段排查',
|
|
children: <JsonBlock value={safeSchema} maxHeight={420} />,
|
|
}]}
|
|
/>
|
|
) : null}
|
|
</Space>
|
|
);
|
|
};
|
|
|
|
export default VideoPromptSchemaViewer;
|