Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 15,997 Bytes
65b0b93 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | """结果区域的 NiceGUI 布局、查询理解和表格显示逻辑。"""
from typing import Any
from nicegui import ui
from core.workspace_insights import (
CANDIDATE_UNSELECTED,
COVERED,
UNCOVERED,
compute_concept_coverage,
)
from webui.constants import OPTIONAL_COLS, TABLE_COLUMNS
from webui.helpers import apply_nsfw_filter
RESULT_TABLE_BODY = r'''
<q-tr :props="props"
:class="props.row._nsfw_blocked ? 'nsfw-row-blocked' : ''"
:style="{
'background-color':
props.row.layer === 'artist' ? 'rgba(244,114,182,0.08)' :
props.row.category === 'General' ? 'rgba(59,130,246,0.06)' :
props.row.category === 'Character' ? 'rgba(34,197,94,0.06)' :
props.row.category === 'Copyright' ? 'rgba(168,85,247,0.06)' : ''
}">
<q-td auto-width>
<q-checkbox v-model="props.selected"
:class="props.row._nsfw_blocked ? 'nsfw-checkbox-disabled' : ''"/>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<template v-if="col.name === 'tag' || col.name === 'cn_name'">
<div :class="props.row._nsfw_blocked ? 'nsfw-blur-cell' : ''">
<template v-if="col.name === 'cn_name' && col.value && props.row.layer !== 'artist'">
<span style="font-size:14px;display:inline-flex;align-items:center;gap:4px;">
<span>{{ col.value.split(',')[0] }}</span>
<q-btn icon="report_problem"
size="sm"
dense flat round
color="grey-5"
padding="xs"
@click.stop.prevent="console.debug('[DanbooruSearch] translation_feedback click', props.row); $parent.$emit('translation_feedback', props.row)">
<q-tooltip>反馈翻译错误</q-tooltip>
</q-btn>
</span>
</template>
<template v-else-if="col.name === 'tag'">
<a :href="'https://danbooru.donmai.us/wiki_pages/'+col.value"
target="_blank"
class="text-primary hover:underline font-bold inline-flex items-center"
style="text-decoration:none; font-family: Consolas, Monaco, Courier New, monospace;"
@click.stop="$emit('link_click', col.value)">
{{ col.value }}
<q-icon name="open_in_new" size="xs" class="q-ml-xs opacity-50"/>
</a>
</template>
<template v-else>{{ col.value }}</template>
</div>
</template>
<template v-else-if="col.name === 'nsfw'">
<div v-if="col.value === '1'" class="text-red-500">🔴</div>
<div v-else class="text-green-500">🟢</div>
</template>
<template v-else-if="col.name === 'final_score'">
<q-badge :color="col.value > 0.6 ? 'green' : (col.value > 0.5 ? 'teal' : 'orange')">
{{ col.value }}
</q-badge>
</template>
<template v-else>{{ col.value }}</template>
</q-td>
<q-tooltip v-if="props.row.layer === 'artist' && props.row.artist_top_tags && props.row.artist_top_tags.length && !props.row._nsfw_blocked"
content-class="bg-black text-white shadow-4"
max-width="400px" :offset="[10,10]">
<div style="font-size:14px;line-height:1.5;max-width:380px;">
<b>{{ props.row.tag }}</b><br>这位画师经常画:<br>
<template v-for="tag in props.row.artist_top_tags.slice(0, 10)" :key="tag">
· {{ tag }}<br>
</template>
</div>
</q-tooltip>
<q-tooltip v-else-if="(props.row.wiki || props.row.cn_name) && !props.row._nsfw_blocked"
content-class="bg-black text-white shadow-4"
max-width="500px" :offset="[10,10]">
<div style="font-size:14px;line-height:1.5;">
<span style="opacity:0.7;margin-right:4px;">{{
props.row.category === 'General' ? '[通用]' :
props.row.category === 'Character' ? '[角色]' :
props.row.category === 'Copyright' ? '[作品]' : ''
}}</span>{{ props.row.wiki }}
<div v-if="props.row.cn_name"
style="margin-top:6px;opacity:0.85;">{{ props.row.cn_name }}</div>
</div>
</q-tooltip>
</q-tr>
'''
def build_results_columns(controller: Any) -> None:
"""构建搜索结果与三个推荐区的静态容器。"""
controller.two_col_container = ui.element('div').classes('w-full two-col-layout')
with controller.two_col_container:
with ui.card().classes('col-left section-surface'):
with ui.row().classes('items-center justify-between mb-2 w-full'):
ui.label('匹配标签结果').classes('section-heading')
ui.button('复制全部标签', icon='content_copy', on_click=controller._copy_all_tags) \
.props('dense flat color=primary').classes('text-sm')
controller.result_table = ui.table(
columns=TABLE_COLUMNS,
rows=[],
pagination=0,
selection='multiple',
row_key='tag',
).props('flat separator=horizontal').classes('w-full result-table-flat')
controller.result_table.on('selection', controller._update_selection_display)
controller.result_table.on('link_click', controller._mark_interaction)
controller.result_table.on('translation_feedback', controller.report_translation_error)
controller.result_table.on('pagination', lambda _: controller._save_config())
controller.result_table.add_slot('body', RESULT_TABLE_BODY)
ui.separator().classes('my-2')
with ui.row().classes('items-center justify-between w-full mb-1'):
with ui.row().classes('items-center gap-2'):
ui.label('同类标签').classes('font-bold text-sm text-gray-600')
with ui.icon('info_outline', size='xs', color='grey').classes('cursor-help'):
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
ui.label('基于标签分组数据,展示已选标签所属分组中的其他标签。勾选可加入已选。').style('font-size:14px;')
ui.button('根据已选刷新', icon='refresh', on_click=controller._manual_refresh_group) \
.props('dense flat color=primary').classes('text-sm')
controller.group_expansion_container = ui.column().classes('w-full gap-0')
controller.group_expansion_container.props('id="danbooru-group-expansion"')
with controller.group_expansion_container:
ui.label('请先搜索并勾选标签…').classes('text-sm text-gray-400 italic p-4')
with ui.card().classes('col-right section-surface'):
with ui.row().classes('items-center justify-between w-full mb-2'):
with ui.row().classes('items-center gap-2'):
ui.label('推荐擅长画师(Beta)').classes('section-heading')
with ui.icon('info_outline', size='sm', color='grey').classes('cursor-help'):
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
ui.html(
'基于标签-画师 NPMI 共现数据,根据您当前已选的标签,推荐擅长这些元素的画师。<br>悬停画师行可查看与该画师共现关联最强的标签。'
).style('font-size:14px;line-height:1.5;')
controller.artist_rec_list = ui.column().classes(
'w-full gap-0 recommendation-grid'
).props('id="danbooru-artist-recommendations"')
with controller.artist_rec_list:
ui.label('请先搜索并勾选标签…').classes('text-sm text-gray-400 italic p-4')
controller.artist_rec_pagination = ui.column().classes('w-full')
ui.separator().classes('my-3')
with ui.row().classes('items-center justify-between w-full mb-2'):
with ui.row().classes('items-center gap-2'):
ui.label('关联推荐').classes('section-heading')
with ui.icon('info_outline', size='sm', color='grey').classes('cursor-help'):
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
ui.html(
'基于标签共现数据,发掘语义之外的相关性,为您推荐更多可能的标签。<br>勾选可加入或移出已选。如需根据最新选项更新推荐,请点击刷新按钮。'
).style('font-size:14px;line-height:1.5;')
ui.button('根据已选刷新', icon='refresh', on_click=controller._manual_refresh_related) \
.props('dense flat color=primary').classes('text-sm')
controller.related_list_container = ui.column().classes(
'w-full gap-0 recommendation-grid'
).props('id="danbooru-related-recommendations"')
with controller.related_list_container:
ui.label('请先搜索并勾选标签…').classes('text-sm text-gray-400 italic p-4')
controller.related_pagination = ui.column().classes('w-full')
def filter_by_source(controller: Any, keyword: str) -> None:
"""按查询概念过滤表格,并重建查询理解卡片。"""
controller.current_filter_keyword = keyword if keyword else 'ALL'
show_nsfw = controller.input_nsfw.value
if not keyword or keyword == 'ALL':
filtered = controller.full_table_data
else:
filtered = [row for row in controller.full_table_data if row['source'] == keyword]
controller.result_table.rows = apply_nsfw_filter(filtered, show_nsfw)
controller._render_concept_coverage()
def render_concept_coverage(controller: Any) -> None:
"""渲染查询概念覆盖状态与概念筛选入口。"""
if controller.coverage_container is None:
return
controller.coverage_container.clear()
if not controller.current_query_str:
return
segments = list(dict.fromkeys(controller.current_segments + controller.current_keywords))
if not segments:
segments = [controller.current_query_str]
coverage = compute_concept_coverage(
segments, controller.full_table_data, controller._get_selected_tags()
)
if not coverage:
return
status_counts = {
COVERED: sum(item.status == COVERED for item in coverage),
CANDIDATE_UNSELECTED: sum(item.status == CANDIDATE_UNSELECTED for item in coverage),
UNCOVERED: sum(item.status == UNCOVERED for item in coverage),
}
def decorate_chip(chip: Any, source: str) -> None:
if source in controller.current_cached_queries:
chip.style('outline: 1px dashed rgba(100,116,139,0.45); outline-offset: 1px;')
if source == controller.current_filter_keyword:
chip.style('box-shadow: 0 0 0 2px #4a90e2;')
with controller.coverage_container:
with ui.element('div').classes('w-full query-insight-panel section-surface'):
with ui.row().classes('w-full items-center gap-2 flex-wrap'):
ui.icon('manage_search', color='primary', size='sm')
ui.label('查询理解').classes('section-heading')
ui.label(
f"已覆盖 {status_counts[COVERED]} · "
f"有候选 {status_counts[CANDIDATE_UNSELECTED]} · "
f"未覆盖 {status_counts[UNCOVERED]}"
).classes('text-xs text-slate-500')
with ui.icon('info_outline', size='xs', color='grey').classes('cursor-help'):
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
ui.label(
'点击概念可筛选对应结果;红色未覆盖项点击后会发起补充搜索。'
'覆盖状态根据当前结果来源和工作区已选标签近似判断。'
).style('font-size:13px;')
with ui.row().classes('w-full gap-2 flex-wrap mt-2'):
all_chip = ui.chip('全部结果', on_click=lambda: controller._filter_by_source('ALL'))
if controller.current_filter_keyword == 'ALL':
all_chip.props('color=primary text-color=white clickable')
else:
all_chip.props('color=grey-3 text-color=grey-9 clickable')
use_segmentation = controller.input_segment.value if controller.input_segment else True
if use_segmentation and controller.current_query_str not in segments:
whole_chip = ui.chip(
'整句',
on_click=lambda: controller._filter_by_source(controller.current_query_str),
).props('color=blue-grey-1 text-color=blue-grey-9 clickable')
decorate_chip(whole_chip, controller.current_query_str)
for item in coverage:
if item.status == COVERED:
chip = ui.chip(
item.segment,
icon='check_circle',
on_click=lambda s=item.segment: controller._filter_by_source(s),
).props('color=green-1 text-color=green-9 clickable')
detail = f"已覆盖;已选择:{'、'.join(item.selected_tags)}。点击筛选此概念的搜索结果。"
elif item.status == CANDIDATE_UNSELECTED:
chip = ui.chip(
item.segment,
icon='radio_button_unchecked',
on_click=lambda s=item.segment: controller._filter_by_source(s),
).props('color=amber-1 text-color=amber-9 clickable')
detail = f"有候选:{'、'.join(item.candidate_tags[:5])}。点击筛选此概念的搜索结果。"
else:
chip = ui.chip(
item.segment,
icon='search',
on_click=lambda s=item.segment: controller._search_uncovered_segment(s),
).props('color=red-1 text-color=red-8 clickable')
detail = '点击后沿用当前搜索设置进行补充搜索;工作区标签保持不变。'
decorate_chip(chip, item.segment)
with chip:
with ui.tooltip().props('content-class="bg-black text-white shadow-4"'):
ui.label(detail).style('font-size:13px;')
def update_table_columns(controller: Any, _event: Any = None) -> None:
"""按显示开关更新结果表的可选列。"""
columns = list(TABLE_COLUMNS)
if controller.sw_semantic and controller.sw_semantic.value:
columns.append(OPTIONAL_COLS['semantic'])
if controller.sw_layer and controller.sw_layer.value:
columns.append(OPTIONAL_COLS['layer'])
if controller.sw_source and controller.sw_source.value:
columns.append(OPTIONAL_COLS['source'])
controller.result_table.columns = columns
|