|
@@ -4,25 +4,79 @@ import { generateTopicTableQueries, getTopicTableQueryPrompt } from '../../api/w
|
|
|
const EXAMPLE = {
|
|
const EXAMPLE = {
|
|
|
topicBuildId: '1229',
|
|
topicBuildId: '1229',
|
|
|
topicId: '1392',
|
|
topicId: '1392',
|
|
|
- maxQueries: '18',
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const ROUTE_ORDER = [
|
|
|
|
|
- 'unique_hook',
|
|
|
|
|
- 'goal_execution',
|
|
|
|
|
- 'key_execution',
|
|
|
|
|
- 'cross_dimension',
|
|
|
|
|
- 'account_fit',
|
|
|
|
|
- 'evidence_gap',
|
|
|
|
|
|
|
+const ROUTES = [
|
|
|
|
|
+ { key: 'unique_hook', inputTitle: '灵感点和特殊组合' },
|
|
|
|
|
+ { key: 'goal_execution', inputTitle: '目标和想达到的效果' },
|
|
|
|
|
+ { key: 'key_execution', inputTitle: '关键镜头和动作要求' },
|
|
|
|
|
+ { key: 'cross_dimension', inputTitle: '实质、形式、意图三类元素' },
|
|
|
|
|
+ { key: 'account_fit', inputTitle: '账号原来的内容风格' },
|
|
|
|
|
+ { key: 'evidence_gap', inputTitle: '参考案例和元素来源' },
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
|
|
+function shortText(value, max = 120) {
|
|
|
|
|
+ const text = String(value || '').trim()
|
|
|
|
|
+ return text.length > max ? `${text.slice(0, max)}…` : text
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function firstItems(values, max = 4) {
|
|
|
|
|
+ return [...new Set((values || []).filter(Boolean).map((value) => shortText(value)))].slice(0, max)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function joinNames(values, max = 8) {
|
|
|
|
|
+ const names = values || []
|
|
|
|
|
+ if (names.length <= max) return names.join('、')
|
|
|
|
|
+ return `${names.slice(0, max).join('、')}等`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function sourceItems(result, route) {
|
|
|
|
|
+ const topic = result.topic || {}
|
|
|
|
|
+ const groups = topic.input_groups || {}
|
|
|
|
|
+ const points = groups.points || {}
|
|
|
|
|
+ const dimensions = topic.dimensions || {}
|
|
|
|
|
+ const direction = topic.topic_direction || {}
|
|
|
|
|
+ const preferences = direction.persona_dimensions || {}
|
|
|
|
|
+
|
|
|
|
|
+ if (route === 'unique_hook') {
|
|
|
|
|
+ return firstItems(points['灵感点'] || [topic.result])
|
|
|
|
|
+ }
|
|
|
|
|
+ if (route === 'goal_execution') {
|
|
|
|
|
+ return firstItems([
|
|
|
|
|
+ ...(points['目的点'] || []),
|
|
|
|
|
+ dimensions['意图']?.length ? `想达到的效果:${joinNames(dimensions['意图'])}` : '',
|
|
|
|
|
+ ])
|
|
|
|
|
+ }
|
|
|
|
|
+ if (route === 'key_execution') {
|
|
|
|
|
+ return firstItems([
|
|
|
|
|
+ ...(points['关键点'] || []),
|
|
|
|
|
+ dimensions['形式']?.length ? `拍摄形式:${joinNames(dimensions['形式'])}` : '',
|
|
|
|
|
+ ])
|
|
|
|
|
+ }
|
|
|
|
|
+ if (route === 'cross_dimension') {
|
|
|
|
|
+ return firstItems([
|
|
|
|
|
+ dimensions['实质']?.length ? `内容里有什么:${joinNames(dimensions['实质'])}` : '',
|
|
|
|
|
+ dimensions['形式']?.length ? `怎么呈现:${joinNames(dimensions['形式'])}` : '',
|
|
|
|
|
+ dimensions['意图']?.length ? `想达到什么效果:${joinNames(dimensions['意图'])}` : '',
|
|
|
|
|
+ ])
|
|
|
|
|
+ }
|
|
|
|
|
+ if (route === 'account_fit') {
|
|
|
|
|
+ return firstItems([
|
|
|
|
|
+ preferences['实质偏好'] ? `常拍内容:${preferences['实质偏好']}` : '',
|
|
|
|
|
+ preferences['形式偏好'] ? `常用拍法:${preferences['形式偏好']}` : '',
|
|
|
|
|
+ preferences['意图偏好'] ? `希望观众感受到:${preferences['意图偏好']}` : '',
|
|
|
|
|
+ ], 3)
|
|
|
|
|
+ }
|
|
|
|
|
+ return firstItems((groups.reference_notes || []).map((row) => `${row.element}:${row.note}`), 3)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function PromptModal({ payload, loading, error, onClose }) {
|
|
function PromptModal({ payload, loading, error, onClose }) {
|
|
|
return (
|
|
return (
|
|
|
<div className="manual-modal-mask" onClick={onClose}>
|
|
<div className="manual-modal-mask" onClick={onClose}>
|
|
|
<section className="manual-modal-panel topic-prompt-modal" onClick={(event) => event.stopPropagation()}>
|
|
<section className="manual-modal-panel topic-prompt-modal" onClick={(event) => event.stopPropagation()}>
|
|
|
<header className="manual-modal-head">
|
|
<header className="manual-modal-head">
|
|
|
<div>
|
|
<div>
|
|
|
- <span className="topic-eyebrow">选题表 → Knowledge Need → Query</span>
|
|
|
|
|
|
|
+ <span className="topic-eyebrow">选题表信息 → 大模型判断 → 搜索词</span>
|
|
|
<h2>生成 Prompt</h2>
|
|
<h2>生成 Prompt</h2>
|
|
|
<p>{payload ? `${payload.name} · ${payload.version}` : '读取当前线上使用的完整 Prompt'}</p>
|
|
<p>{payload ? `${payload.name} · ${payload.version}` : '读取当前线上使用的完整 Prompt'}</p>
|
|
|
</div>
|
|
</div>
|
|
@@ -45,103 +99,43 @@ function PromptModal({ payload, loading, error, onClose }) {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function SourceCard({ result }) {
|
|
|
|
|
- const topic = result.topic || {}
|
|
|
|
|
- const direction = topic.topic_direction || {}
|
|
|
|
|
- const dimensions = topic.dimensions || {}
|
|
|
|
|
- return (
|
|
|
|
|
- <section className="topic-flow-column topic-source-column">
|
|
|
|
|
- <div className="topic-flow-head">
|
|
|
|
|
- <span className="topic-step">01</span>
|
|
|
|
|
- <div>
|
|
|
|
|
- <strong>选题表提供的信息</strong>
|
|
|
|
|
- <small>真实选题与账号约束</small>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="topic-source-card">
|
|
|
|
|
- <div className="topic-source-meta">
|
|
|
|
|
- <span>Build {result.source?.topic_build_id}</span>
|
|
|
|
|
- <span>Topic {result.source?.topic_id}</span>
|
|
|
|
|
- <span>{topic.status || 'unknown'}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <h3>{direction.account_name || '选题结果'}</h3>
|
|
|
|
|
- <p>{topic.result}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- {['实质', '形式', '意图'].map((dimension) => (
|
|
|
|
|
- <div className="topic-dimension-group" key={dimension}>
|
|
|
|
|
- <strong>{dimension}</strong>
|
|
|
|
|
- <div className="topic-chip-list">
|
|
|
|
|
- {(dimensions[dimension] || []).map((name) => <span key={`${dimension}-${name}`}>{name}</span>)}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
- <div className="topic-point-list">
|
|
|
|
|
- <strong>选题点</strong>
|
|
|
|
|
- {(topic.points || []).filter((point) => point.is_active !== false).map((point) => (
|
|
|
|
|
- <div key={point.id}>
|
|
|
|
|
- <span>{point.point_type}</span>
|
|
|
|
|
- <p>{point.point_result}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
- </div>
|
|
|
|
|
- </section>
|
|
|
|
|
- )
|
|
|
|
|
-}
|
|
|
|
|
|
|
+function GenerationRows({ result }) {
|
|
|
|
|
+ const rows = useMemo(() => ROUTES.map((route, index) => ({
|
|
|
|
|
+ ...route,
|
|
|
|
|
+ number: index + 1,
|
|
|
|
|
+ need: (result.knowledge_needs || []).find((need) => need.route === route.key),
|
|
|
|
|
+ queries: (result.queries || []).filter((query) => query.route === route.key),
|
|
|
|
|
+ inputs: sourceItems(result, route.key),
|
|
|
|
|
+ })), [result])
|
|
|
|
|
|
|
|
-function NeedsColumn({ result }) {
|
|
|
|
|
- const grouped = useMemo(() => {
|
|
|
|
|
- const rows = new Map()
|
|
|
|
|
- for (const need of result.knowledge_needs || []) {
|
|
|
|
|
- rows.set(need.route, [...(rows.get(need.route) || []), need])
|
|
|
|
|
- }
|
|
|
|
|
- return rows
|
|
|
|
|
- }, [result])
|
|
|
|
|
return (
|
|
return (
|
|
|
- <section className="topic-flow-column topic-needs-column">
|
|
|
|
|
- <div className="topic-flow-head">
|
|
|
|
|
- <span className="topic-step">02</span>
|
|
|
|
|
- <div>
|
|
|
|
|
- <strong>LLM 判断真正未知</strong>
|
|
|
|
|
- <small>六条路径形成 Knowledge Need</small>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <section className="topic-route-map">
|
|
|
|
|
+ <div className="topic-route-map-head">
|
|
|
|
|
+ <strong>选题表里用到的信息</strong>
|
|
|
|
|
+ <strong>大模型判断什么</strong>
|
|
|
|
|
+ <strong>生成哪些具体搜索词</strong>
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="topic-needs-list">
|
|
|
|
|
- {ROUTE_ORDER.flatMap((route) => grouped.get(route) || []).map((need) => (
|
|
|
|
|
- <article className={`topic-need-card route-${need.route}`} key={need.need_key}>
|
|
|
|
|
- <div className="topic-need-title">
|
|
|
|
|
- <span>{need.route_label}</span>
|
|
|
|
|
- <b>P{need.priority}</b>
|
|
|
|
|
|
|
+ <div className="topic-route-map-body">
|
|
|
|
|
+ {rows.map((row) => (
|
|
|
|
|
+ <article className={`topic-route-row route-${row.key}`} key={row.key}>
|
|
|
|
|
+ <div className="topic-route-input">
|
|
|
|
|
+ <span>第 {row.number} 种</span>
|
|
|
|
|
+ <h3>{row.inputTitle}</h3>
|
|
|
|
|
+ <ul>
|
|
|
|
|
+ {row.inputs.map((item) => <li key={item}>{item}</li>)}
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="topic-route-judgement">
|
|
|
|
|
+ <p>{row.need?.decision_context || '这类信息没有形成需要判断的问题'}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="topic-route-queries">
|
|
|
|
|
+ {row.queries.map((query, index) => (
|
|
|
|
|
+ <div key={`${query.query}-${index}`}>
|
|
|
|
|
+ <b>{index + 1}</b>
|
|
|
|
|
+ <p>{query.query}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
</div>
|
|
</div>
|
|
|
- <p><strong>要决定:</strong>{need.decision_context}</p>
|
|
|
|
|
- <p><strong>真正未知:</strong>{need.unknown_information}</p>
|
|
|
|
|
- </article>
|
|
|
|
|
- ))}
|
|
|
|
|
- </div>
|
|
|
|
|
- </section>
|
|
|
|
|
- )
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function QueryPreviewColumn({ result }) {
|
|
|
|
|
- return (
|
|
|
|
|
- <section className="topic-flow-column topic-query-column">
|
|
|
|
|
- <div className="topic-flow-head">
|
|
|
|
|
- <span className="topic-step">03</span>
|
|
|
|
|
- <div>
|
|
|
|
|
- <strong>原子 Query</strong>
|
|
|
|
|
- <small>标准化、精确去重、预算后的结果</small>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="topic-query-summary">
|
|
|
|
|
- <span><b>{result.summary?.selected_count || 0}</b> 条 Query</span>
|
|
|
|
|
- <span>{result.summary?.unique_count || 0} 条去重后</span>
|
|
|
|
|
- <em>未搜索</em>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="topic-query-list">
|
|
|
|
|
- {(result.queries || []).map((query, index) => (
|
|
|
|
|
- <article key={`${query.query}-${index}`}>
|
|
|
|
|
- <span className={`topic-query-route route-${query.route}`}>{query.route_label}</span>
|
|
|
|
|
- <p>{query.query}</p>
|
|
|
|
|
- <small>{query.axes?.未知信息}</small>
|
|
|
|
|
</article>
|
|
</article>
|
|
|
))}
|
|
))}
|
|
|
</div>
|
|
</div>
|
|
@@ -152,7 +146,6 @@ function QueryPreviewColumn({ result }) {
|
|
|
export default function TopicTableQueryPanel() {
|
|
export default function TopicTableQueryPanel() {
|
|
|
const [topicBuildId, setTopicBuildId] = useState(EXAMPLE.topicBuildId)
|
|
const [topicBuildId, setTopicBuildId] = useState(EXAMPLE.topicBuildId)
|
|
|
const [topicId, setTopicId] = useState(EXAMPLE.topicId)
|
|
const [topicId, setTopicId] = useState(EXAMPLE.topicId)
|
|
|
- const [maxQueries, setMaxQueries] = useState(EXAMPLE.maxQueries)
|
|
|
|
|
const [result, setResult] = useState(null)
|
|
const [result, setResult] = useState(null)
|
|
|
const [loading, setLoading] = useState(false)
|
|
const [loading, setLoading] = useState(false)
|
|
|
const [error, setError] = useState('')
|
|
const [error, setError] = useState('')
|
|
@@ -183,11 +176,11 @@ export default function TopicTableQueryPanel() {
|
|
|
const payload = {
|
|
const payload = {
|
|
|
topic_build_id: Number(topicBuildId),
|
|
topic_build_id: Number(topicBuildId),
|
|
|
topic_id: topicId ? Number(topicId) : null,
|
|
topic_id: topicId ? Number(topicId) : null,
|
|
|
- max_queries: Number(maxQueries),
|
|
|
|
|
|
|
+ max_queries: 18,
|
|
|
}
|
|
}
|
|
|
setResult(await generateTopicTableQueries(payload))
|
|
setResult(await generateTopicTableQueries(payload))
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
- setError(e.message || 'Query 生成失败')
|
|
|
|
|
|
|
+ setError(e.message || '搜索词生成失败')
|
|
|
} finally {
|
|
} finally {
|
|
|
setLoading(false)
|
|
setLoading(false)
|
|
|
}
|
|
}
|
|
@@ -198,61 +191,55 @@ export default function TopicTableQueryPanel() {
|
|
|
<header className="topic-table-intro">
|
|
<header className="topic-table-intro">
|
|
|
<div>
|
|
<div>
|
|
|
<span className="topic-eyebrow">第四种生成方式</span>
|
|
<span className="topic-eyebrow">第四种生成方式</span>
|
|
|
- <h2>从选题表找出真正缺少的创作知识</h2>
|
|
|
|
|
- <p>读取选题字段,交给 LLM 沿六条路径判断 Knowledge Need,再拆成原子 Query。</p>
|
|
|
|
|
|
|
+ <h2>根据选题表生成搜索词</h2>
|
|
|
|
|
+ <p>每一类选题信息,都对应一项大模型判断和一组具体搜索词。</p>
|
|
|
</div>
|
|
</div>
|
|
|
- <span className="topic-preview-only">仅生成 Query · 不创建 Batch · 不启动搜索</span>
|
|
|
|
|
|
|
+ <span className="topic-preview-only">这里只生成搜索词,不会开始搜索</span>
|
|
|
</header>
|
|
</header>
|
|
|
|
|
|
|
|
<form className="topic-table-form" onSubmit={generate}>
|
|
<form className="topic-table-form" onSubmit={generate}>
|
|
|
<label>
|
|
<label>
|
|
|
- <span>选题构建 ID</span>
|
|
|
|
|
|
|
+ <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)} type="number" min="1" required />
|
|
|
</label>
|
|
</label>
|
|
|
<label>
|
|
<label>
|
|
|
- <span>选题 ID</span>
|
|
|
|
|
|
|
+ <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)} type="number" min="1" placeholder="不填则取 mature 选题" />
|
|
|
</label>
|
|
</label>
|
|
|
- <label>
|
|
|
|
|
- <span>Query 预算</span>
|
|
|
|
|
- <input value={maxQueries} onChange={(e) => setMaxQueries(e.target.value)} type="number" min="6" max="60" required />
|
|
|
|
|
- </label>
|
|
|
|
|
<button className="topic-example-button" type="button" onClick={() => {
|
|
<button className="topic-example-button" type="button" onClick={() => {
|
|
|
setTopicBuildId(EXAMPLE.topicBuildId)
|
|
setTopicBuildId(EXAMPLE.topicBuildId)
|
|
|
setTopicId(EXAMPLE.topicId)
|
|
setTopicId(EXAMPLE.topicId)
|
|
|
- setMaxQueries(EXAMPLE.maxQueries)
|
|
|
|
|
setResult(null)
|
|
setResult(null)
|
|
|
setError('')
|
|
setError('')
|
|
|
}}>载入真实示例 1229</button>
|
|
}}>载入真实示例 1229</button>
|
|
|
<button className="topic-prompt-button" type="button" onClick={showPrompt}>查看生成 Prompt</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}>
|
|
|
- {loading ? 'LLM 正在判断需求…' : '生成 Query 预览'}
|
|
|
|
|
|
|
+ {loading ? '大模型正在生成…' : '生成搜索词'}
|
|
|
</button>
|
|
</button>
|
|
|
</form>
|
|
</form>
|
|
|
|
|
|
|
|
{error && <div className="manual-error topic-generation-error">{error}</div>}
|
|
{error && <div className="manual-error topic-generation-error">{error}</div>}
|
|
|
{!result && !loading && (
|
|
{!result && !loading && (
|
|
|
- <section className="topic-example-card">
|
|
|
|
|
- <div>
|
|
|
|
|
- <span>真实示例</span>
|
|
|
|
|
- <strong>1229 / 1392 · 樱花树下少女与小羊互动穿搭摄影</strong>
|
|
|
|
|
- <p>系统会从低角度、彩虹光晕、治愈感、好物分享等真实字段判断还缺哪些创作知识。</p>
|
|
|
|
|
|
|
+ <section className="topic-example-map">
|
|
|
|
|
+ <div className="topic-example-cell">
|
|
|
|
|
+ <span>第 1 种 · 灵感点和特殊组合</span>
|
|
|
|
|
+ <p>樱花树下低角度拍小羊,用 CCD 记录彩虹光晕</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="topic-example-cell">
|
|
|
|
|
+ <span>大模型判断什么</span>
|
|
|
|
|
+ <p>这个画面里的彩虹光晕,怎样才能稳定拍出来?</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="topic-example-cell">
|
|
|
|
|
+ <span>生成的搜索词</span>
|
|
|
|
|
+ <ul>
|
|
|
|
|
+ <li>逆光人像怎么拍出自然彩虹光晕</li>
|
|
|
|
|
+ <li>低角度拍摄时太阳和镜头怎么配合</li>
|
|
|
|
|
+ </ul>
|
|
|
</div>
|
|
</div>
|
|
|
- <ul>
|
|
|
|
|
- <li>逆光人像怎么拍出彩虹光晕</li>
|
|
|
|
|
- <li>人物和小羊互动怎么拍得自然</li>
|
|
|
|
|
- <li>氛围感穿搭图文如何自然展示配饰</li>
|
|
|
|
|
- </ul>
|
|
|
|
|
</section>
|
|
</section>
|
|
|
)}
|
|
)}
|
|
|
- {loading && <div className="topic-loading-card"><span />正在读取选题表并生成六路 Knowledge Need…</div>}
|
|
|
|
|
- {result && (
|
|
|
|
|
- <div className="topic-flow-grid">
|
|
|
|
|
- <SourceCard result={result} />
|
|
|
|
|
- <NeedsColumn result={result} />
|
|
|
|
|
- <QueryPreviewColumn result={result} />
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ {loading && <div className="topic-loading-card"><span />正在读取选题表,并逐类生成搜索词…</div>}
|
|
|
|
|
+ {result && <GenerationRows result={result} />}
|
|
|
{promptOpen && (
|
|
{promptOpen && (
|
|
|
<PromptModal
|
|
<PromptModal
|
|
|
payload={prompt}
|
|
payload={prompt}
|