|
|
@@ -1,74 +1,11 @@
|
|
|
-import { useState } from 'react'
|
|
|
-import { generateTopicTableQueries, getTopicTableQueryPrompt } from '../../api/workbench.js'
|
|
|
+import { useCallback, useEffect, useState } from 'react'
|
|
|
+import { generateTopicTableQueries, getTopicTableQueryPrompt, getTopicTableSources } from '../../api/workbench.js'
|
|
|
|
|
|
const EXAMPLE = {
|
|
|
topicBuildId: '1229',
|
|
|
topicId: '1392',
|
|
|
}
|
|
|
|
|
|
-const EXAMPLE_GROUPS = [
|
|
|
- {
|
|
|
- key: 'unique_hook', label: '灵感点', source_description: '来自选题的灵感点字段',
|
|
|
- field_mappings: [{
|
|
|
- field_key: 'point:8371:point_result', field_label: '灵感点字段',
|
|
|
- field_path: 'topics[id=1392].points[id=8371].point_result',
|
|
|
- field_value: '在樱花树下低角度仰拍小羊,用CCD记录折射出的彩虹光晕',
|
|
|
- judgement: '这条灵感里的逆光、动物和低角度画面怎样实际拍出来',
|
|
|
- queries: ['樱花树下低角度拍小羊怎么安排机位'],
|
|
|
- }],
|
|
|
- },
|
|
|
- {
|
|
|
- key: 'goal_execution', label: '创作目标', source_description: '来自目的点字段',
|
|
|
- field_mappings: [{
|
|
|
- field_key: 'point:8372:point_result', field_label: '目的点字段',
|
|
|
- field_path: 'topics[id=1392].points[id=8372].point_result', field_value: '传递治愈感',
|
|
|
- judgement: '治愈感要通过哪些画面和人物状态表现',
|
|
|
- queries: ['人像摄影怎么通过光线和动作表现治愈感'],
|
|
|
- }],
|
|
|
- },
|
|
|
- {
|
|
|
- key: 'key_execution', label: '关键镜头和动作', source_description: '来自关键点字段',
|
|
|
- field_mappings: [{
|
|
|
- field_key: 'point:8374:point_result', field_label: '关键点字段',
|
|
|
- field_path: 'topics[id=1392].points[id=8374].point_result', field_value: '逆光氛围感特写',
|
|
|
- judgement: '逆光特写怎样保住人物细节又有氛围',
|
|
|
- queries: ['逆光人像特写怎么避免脸部过暗'],
|
|
|
- }],
|
|
|
- },
|
|
|
- {
|
|
|
- key: 'cross_dimension', label: '实质、形式、意图及元素关系',
|
|
|
- source_description: '来自组成元素、最终选题描述和元素关系字段',
|
|
|
- field_mappings: [{
|
|
|
- field_key: 'item:25686:element_name', field_label: '实质字段',
|
|
|
- field_path: 'topics[id=1392].composition_items[id=25686].element_name', field_value: '彩虹光晕',
|
|
|
- judgement: '彩虹光晕怎样在真实拍摄中稳定出现',
|
|
|
- queries: ['CCD逆光拍摄怎么拍出自然彩虹光晕'],
|
|
|
- }],
|
|
|
- },
|
|
|
- {
|
|
|
- key: 'account_fit', label: '账号内容偏好',
|
|
|
- source_description: '来自账号的实质偏好、形式偏好和意图偏好字段',
|
|
|
- field_mappings: [{
|
|
|
- field_key: 'topic_direction.persona_dimensions.形式偏好', field_label: '账号形式偏好',
|
|
|
- field_path: 'topics[id=1392].topic_direction.persona_dimensions.形式偏好',
|
|
|
- field_value: '低角度仰拍、广角、特写细节、动态摆拍和人景互动',
|
|
|
- judgement: '这次内容怎样沿用账号常用的镜头和动作',
|
|
|
- queries: ['穿搭摄影怎么组合低角度全景和细节特写'],
|
|
|
- }],
|
|
|
- },
|
|
|
- {
|
|
|
- key: 'evidence_gap', label: '参考案例和选取理由',
|
|
|
- source_description: '来自每个元素引用的参考来源说明字段',
|
|
|
- field_mappings: [{
|
|
|
- field_key: 'item:25686:source:0:reason', field_label: '参考来源说明',
|
|
|
- field_path: 'topics[id=1392].composition_items[id=25686].sources[0].reason',
|
|
|
- field_value: '案例显示“彩虹印在脸上”能增强画面的惊艳感和新鲜度',
|
|
|
- judgement: '案例里的彩虹光影换到樱花和小羊场景后怎样调整',
|
|
|
- queries: ['彩虹投影人像换到户外逆光场景怎么调整'],
|
|
|
- }],
|
|
|
- },
|
|
|
-]
|
|
|
-
|
|
|
function PromptModal({ payload, loading, error, onClose }) {
|
|
|
return (
|
|
|
<div className="manual-modal-mask" onClick={onClose}>
|
|
|
@@ -91,6 +28,7 @@ function PromptModal({ payload, loading, error, onClose }) {
|
|
|
|
|
|
function GenerationRows({ result }) {
|
|
|
const groups = result.source_groups || []
|
|
|
+ const generationCompleted = Boolean(result.summary?.query_generation_completed)
|
|
|
const [openGroups, setOpenGroups] = useState(() => ({ unique_hook: true }))
|
|
|
|
|
|
const toggleGroup = (key) => {
|
|
|
@@ -144,7 +82,7 @@ function GenerationRows({ result }) {
|
|
|
<div className="topic-route-judgement">
|
|
|
{usedForQuery
|
|
|
? <p>{row.judgement}</p>
|
|
|
- : <p className="topic-unused-copy">本次未使用这个字段生成搜索词</p>}
|
|
|
+ : <p className="topic-unused-copy">{generationCompleted ? '本次未使用这个字段生成搜索词' : '尚未让大模型判断这个字段'}</p>}
|
|
|
</div>
|
|
|
<div className="topic-route-queries">
|
|
|
{usedForQuery
|
|
|
@@ -154,7 +92,7 @@ function GenerationRows({ result }) {
|
|
|
<p>{query}</p>
|
|
|
</div>
|
|
|
))
|
|
|
- : <p className="topic-unused-copy">暂无对应搜索词</p>}
|
|
|
+ : <p className="topic-unused-copy">{generationCompleted ? '本次没有生成对应搜索词' : '点击“生成搜索词”后显示结果'}</p>}
|
|
|
</div>
|
|
|
</article>
|
|
|
)
|
|
|
@@ -173,12 +111,37 @@ export default function TopicTableQueryPanel() {
|
|
|
const [topicId, setTopicId] = useState(EXAMPLE.topicId)
|
|
|
const [result, setResult] = useState(null)
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
+ const [sourceLoading, setSourceLoading] = useState(false)
|
|
|
const [error, setError] = useState('')
|
|
|
const [promptOpen, setPromptOpen] = useState(false)
|
|
|
const [prompt, setPrompt] = useState(null)
|
|
|
const [promptLoading, setPromptLoading] = useState(false)
|
|
|
const [promptError, setPromptError] = useState('')
|
|
|
|
|
|
+ const loadSourceData = useCallback(async (buildValue, topicValue, { signal } = {}) => {
|
|
|
+ setSourceLoading(true)
|
|
|
+ setError('')
|
|
|
+ try {
|
|
|
+ const payload = {
|
|
|
+ topic_build_id: Number(buildValue),
|
|
|
+ topic_id: topicValue ? Number(topicValue) : null,
|
|
|
+ max_queries: 18,
|
|
|
+ }
|
|
|
+ const sourceResult = await getTopicTableSources(payload, { signal })
|
|
|
+ setResult(sourceResult)
|
|
|
+ } catch (e) {
|
|
|
+ if (e.name !== 'AbortError') setError(e.message || '选题表完整数据读取失败')
|
|
|
+ } finally {
|
|
|
+ setSourceLoading(false)
|
|
|
+ }
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const controller = new AbortController()
|
|
|
+ loadSourceData(EXAMPLE.topicBuildId, EXAMPLE.topicId, { signal: controller.signal })
|
|
|
+ return () => controller.abort()
|
|
|
+ }, [loadSourceData])
|
|
|
+
|
|
|
const showPrompt = async () => {
|
|
|
setPromptOpen(true)
|
|
|
if (prompt || promptLoading) return
|
|
|
@@ -225,28 +188,26 @@ export default function TopicTableQueryPanel() {
|
|
|
<form className="topic-table-form" onSubmit={generate}>
|
|
|
<label>
|
|
|
<span>选题表编号</span>
|
|
|
- <input value={topicBuildId} onChange={(e) => setTopicBuildId(e.target.value)} type="number" min="1" required />
|
|
|
+ <input value={topicBuildId} onChange={(e) => { setTopicBuildId(e.target.value); setResult(null) }} type="number" min="1" required />
|
|
|
</label>
|
|
|
<label>
|
|
|
<span>选题编号</span>
|
|
|
- <input value={topicId} onChange={(e) => setTopicId(e.target.value)} type="number" min="1" placeholder="不填则取 mature 选题" />
|
|
|
+ <input value={topicId} onChange={(e) => { setTopicId(e.target.value); setResult(null) }} type="number" min="1" placeholder="不填则取 mature 选题" />
|
|
|
</label>
|
|
|
- <button className="topic-example-button" type="button" onClick={() => {
|
|
|
+ <button className="topic-example-button" type="button" disabled={sourceLoading || loading} onClick={() => {
|
|
|
setTopicBuildId(EXAMPLE.topicBuildId)
|
|
|
setTopicId(EXAMPLE.topicId)
|
|
|
- setResult(null)
|
|
|
- setError('')
|
|
|
+ loadSourceData(EXAMPLE.topicBuildId, EXAMPLE.topicId)
|
|
|
}}>载入真实示例 1229</button>
|
|
|
<button className="topic-prompt-button" type="button" onClick={showPrompt}>查看生成 Prompt</button>
|
|
|
- <button className="topic-generate-button" type="submit" disabled={loading}>
|
|
|
+ <button className="topic-generate-button" type="submit" disabled={loading || sourceLoading}>
|
|
|
{loading ? '大模型正在生成…' : '生成搜索词'}
|
|
|
</button>
|
|
|
</form>
|
|
|
|
|
|
{error && <div className="manual-error topic-generation-error">{error}</div>}
|
|
|
- {!result && !loading && (
|
|
|
- <GenerationRows result={{ source_groups: EXAMPLE_GROUPS }} />
|
|
|
- )}
|
|
|
+ {sourceLoading && !result && <div className="topic-loading-card"><span />正在读取1229的完整选题表字段…</div>}
|
|
|
+ {!result && !sourceLoading && !loading && <div className="topic-loading-card">填写选题表编号后,点击“载入真实示例”读取完整字段。</div>}
|
|
|
{loading && <div className="topic-loading-card"><span />正在读取真实字段,并逐字段生成搜索词…</div>}
|
|
|
{result && <GenerationRows result={result} />}
|
|
|
{promptOpen && (
|