ArtShumov commited on
Commit
ef745a5
·
1 Parent(s): eaadfed

style(ui): polish visuals - localize tabs/tooltips, progress bars, light theme, hide empty cards, mobile layout

Browse files
Files changed (3) hide show
  1. app.py +151 -136
  2. src/i18n.py +4 -0
  3. src/prompt_analyzer.py +11 -11
app.py CHANGED
@@ -105,14 +105,21 @@ COPY_JS = """(text) => {
105
  }"""
106
 
107
 
108
- def on_generate(prompt, model, rating, num_variations, creativity, weight_mode, mode, lang, artist_style, selected_artists, use_tandems, selected_tandem_idx, web_enrich, seed, current_preset_state, fx_count, blacklist_text, mirror_blacklist, strip_quality, strip_artist, strip_lora, strip_meta, min_tags, user_preset_state, output_format, *checks) -> tuple[str, ...]:
 
 
 
 
 
 
109
  try:
110
  if not prompt or not prompt.strip():
111
- return tuple([""] * (MAX_OUTPUTS * 2))
112
  parsed = parse_prompt(prompt)
113
  if parsed is None:
114
  msg = t("error_parse", lang)
115
- return tuple([msg] + [""] * (MAX_OUTPUTS - 1) + [""] * MAX_OUTPUTS)
 
116
  active = [c for c, on in zip(ALL_CATEGORIES, checks) if on]
117
  if not active:
118
  active = ["quality", "lighting", "expression"]
@@ -191,12 +198,13 @@ def on_generate(prompt, model, rating, num_variations, creativity, weight_mode,
191
  categories=active,
192
  seed=int(seed) if seed is not None and str(seed).strip() else None,
193
  )
194
- return tuple(out[:MAX_OUTPUTS]) + tuple(neg_out[:MAX_OUTPUTS])
195
  except Exception as exc:
196
  import traceback
197
  traceback.print_exc()
198
  err_msg = t("generation_error", lang).format(exc=html.escape(str(exc)))
199
- return tuple([err_msg] + [""] * (MAX_OUTPUTS - 1) + [""] * MAX_OUTPUTS)
 
200
 
201
 
202
  def on_suggest(prompt, model, rating, lang) -> str:
@@ -262,22 +270,22 @@ def on_web_search(query: str, lang: str) -> str:
262
  from src.tag_searcher import search_tags
263
  lc = "ru" if lang == "RU" else "en"
264
  if not query or not query.strip():
265
- return f'<div class="whyx-info-text" style="color:rgba(148,163,184,0.6);font-size:12px;">{t("web_search_empty", lc)}</div>'
266
  try:
267
  results = search_tags(query, warehouse)
268
  parts = []
269
  if results["local"]:
270
- parts.append(f'<div style="margin-top:6px;font-size:13px;font-weight:600;">🔍 {t("web_search_offline", lc)} ({len(results["local"])})</div>')
271
  for r in results["local"][:15]:
272
  safe_tag = html.escape(r["tag"])
273
  safe_cat = html.escape(r["category"])
274
- parts.append(f'<span style="display:inline-block;background:rgba(79,70,229,0.1);border:1px solid rgba(79,70,229,0.2);border-radius:4px;padding:1px 6px;margin:2px;font-size:12px;">{safe_tag} <span style="color:rgba(148,163,184,0.6);font-size:10px;">[{safe_cat}]</span></span>')
275
  if results["web"]:
276
- parts.append(f'<div style="margin-top:8px;font-size:13px;font-weight:600;">🌐 {t("web_search_online", lc)} ({len(results["web"])})</div>')
277
  for r in results["web"][:MAX_OUTPUTS]:
278
  safe_tag = html.escape(r.get("tag", ""))
279
  count = r.get("post_count", 0)
280
- parts.append(f'<span style="display:inline-block;background:rgba(99,102,241,0.1);border:1px solid rgba(99,102,241,0.2);border-radius:4px;padding:1px 6px;margin:2px;font-size:12px;">{safe_tag} <span style="color:rgba(148,163,184,0.6);font-size:10px;">({count} posts)</span></span>')
281
  if not parts:
282
  return f'<div class="whyx-info-text" style="font-size:12px;">{t("web_enrich_no_results", lc)}</div>'
283
  return '<div style="line-height:1.8;">' + "".join(parts) + '</div>'
@@ -289,7 +297,7 @@ def _format_history_html(lang: str) -> str:
289
  lc = "ru" if lang == "RU" else "en"
290
  history = get_history()
291
  lines = []
292
- lines.append("<div style='background:rgba(15,23,42,0.5);border:1px solid rgba(124,58,237,0.12);border-radius:10px;padding:10px 14px;margin-top:4px;'>")
293
 
294
  entries = history.get_all()[:5]
295
  if not entries:
@@ -490,7 +498,7 @@ def _build_tagger_html(result: dict, lc: str) -> str:
490
  nl_caption = (result.get("nl_caption") or "").strip()
491
  if nl_caption:
492
  lines.append(
493
- f"<div class='whyx-tagger-panel-title'>🗣️ {t('tagger_nl_caption', lc)}</div>"
494
  f"<div class='whyx-tagger-nl-caption'>{html.escape(nl_caption)}</div>"
495
  )
496
 
@@ -534,8 +542,12 @@ def _build_tagger_html(result: dict, lc: str) -> str:
534
  for name, score in result["ratings"].items():
535
  color = rating_colors.get(name, "var(--text-dim)")
536
  pct = int(score * 100)
537
- bar = "▰" * (pct // 10) + "▱" * (10 - pct // 10)
538
- lines.append(f"<div class='whyx-tagger-rating-row'><span class='whyx-tagger-rating-name' style='color:{color};'>{name}</span> <span class='whyx-tagger-rating-bar' style='color:{color};'>{bar}</span> <span class='whyx-tagger-rating-pct'>({score:.1%})</span></div>")
 
 
 
 
539
 
540
  if result["characters"]:
541
  lines.append(f"<div class='whyx-tagger-panel-title' style='margin-top:10px;'>{t('tagger_characters', lc)}</div>")
@@ -549,8 +561,12 @@ def _build_tagger_html(result: dict, lc: str) -> str:
549
  for name, score in list(result["general"].items())[:14]:
550
  safe = html.escape(name.replace("_", " ").replace("(", "\\(").replace(")", "\\)"))
551
  pct = int(score * 100)
552
- bar = "▰" * (pct // 10) + "▱" * (10 - pct // 10)
553
- lines.append(f"<div class='whyx-tagger-gen-row'><span class='whyx-tagger-gen-bar'>{bar}</span><span class='whyx-tagger-gen-name'>{safe}</span><span class='whyx-tagger-badge-pct'>{score:.0%}</span></div>")
 
 
 
 
554
  lines.append("</div>")
555
  lines.append("</div>")
556
  return "\n".join(lines)
@@ -884,6 +900,29 @@ body.whyx-light, body.whyx-light .gradio-container, body.whyx-light .dark {
884
  --button-secondary-background-fill: transparent !important; --button-secondary-background-fill-hover: rgba(15,23,42,0.05) !important;
885
  --button-secondary-border-color: rgba(15,23,42,0.16) !important;
886
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
887
  @keyframes whyxBreath { 0%, 100% { opacity: 0.65; } 50% { opacity: 1; } }
888
  @keyframes whyxFadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
889
  @keyframes progressShimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
@@ -893,25 +932,10 @@ body::before { content: ""; position: fixed; inset: 0; z-index: -1; pointer-even
893
  :focus-visible { outline: 2px solid rgba(90,200,245,0.6) !important; outline-offset: 2px !important; }
894
  *::-webkit-scrollbar { width: 5px; height: 5px; } *::-webkit-scrollbar-track { background: rgba(15,23,42,0.25); } *::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 3px; } *::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.14); }
895
 
896
- .whyx-section-card { background: var(--surface) !important; border: 1px solid var(--glass-border) !important; border-left: 3px solid rgba(110,231,210,0.18) !important; border-radius: var(--radius-lg) !important; box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255,255,255,0.03) !important; transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition) !important; overflow: visible !important; }
897
- .whyx-section-card:hover { border-color: var(--glass-border-hover) !important; border-left-color: rgba(16,185,129,0.4) !important; box-shadow: var(--shadow-lg), var(--shadow-glow) !important; transform: translateY(-1px); }
898
- .whyx-section-header { display: flex !important; align-items: flex-start !important; gap: 10px !important; margin: 0 0 14px !important; padding: 0 0 10px !important; border-bottom: 1px solid var(--glass-border) !important; animation: whyxFadeIn 0.3s ease both !important; }
899
- .whyx-section-icon { font-size: 18px !important; line-height: 1.4 !important; flex-shrink: 0; }
900
- .whyx-section-title { color: var(--text) !important; background: none !important; -webkit-background-clip: unset !important; -webkit-text-fill-color: unset !important; background-clip: unset !important; font-weight: 700 !important; font-size: 11px !important; letter-spacing: 1px !important; text-transform: uppercase !important; line-height: 1.4 !important; }
901
- .whyx-section-desc { color: var(--text-dim) !important; font-size: 11px !important; line-height: 1.45 !important; margin-top: 2px !important; max-width: 640px !important; }
902
  .whyx-title { background: linear-gradient(135deg, #7C3AED, #4F46E5 38%, #2563EB 70%, #10B981) !important; -webkit-background-clip: text !important; -webkit-text-fill-color: transparent !important; background-clip: text !important; color: transparent !important; display: inline-block !important; font-weight: 800 !important; font-size: 32px !important; letter-spacing: -0.6px !important; line-height: 1.2 !important; }
903
  .whyx-subtitle { font-size: 12px !important; color: var(--text-dim) !important; margin-top: 2px !important; border-left: 2px solid rgba(110,231,210,0.45) !important; padding-left: 8px !important; }
904
  .whyx-divider { height: 1px !important; background: linear-gradient(90deg, transparent, rgba(110,231,210,0.2), rgba(176,124,240,0.2), transparent) !important; margin: 10px 0 !important; border: none !important; }
905
 
906
- .whyx-stepper { counter-reset: whyx-step; position: relative; gap: 4px; }
907
- .whyx-stepper::before { content: ""; position: absolute; left: 25px; top: 30px; bottom: 44px; width: 2px; background: linear-gradient(180deg, var(--c0), var(--c1) 50%, var(--c3)); border-radius: 2px; opacity: 0.35; }
908
- .whyx-step { position: relative; counter-increment: whyx-step; background: var(--surface) !important; border: 1px solid var(--glass-border) !important; border-radius: var(--radius-xl) !important; padding: 16px 18px 16px 60px !important; margin-bottom: 10px !important; box-shadow: var(--shadow-sm) !important; transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition) !important; }
909
- .whyx-step:hover { border-color: var(--glass-border-hover) !important; box-shadow: var(--shadow-md), 0 0 16px rgba(79,70,229,0.08) !important; transform: translateY(-1px); }
910
- .whyx-step::before { content: counter(whyx-step); position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 32px; height: 32px; border-radius: 50%; background: var(--accent); color: #06121A; font-weight: 800; font-size: 14px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 14px rgba(79,70,229,0.25); z-index: 2; }
911
- .whyx-step:hover::before { box-shadow: 0 6px 20px rgba(79,70,229,0.4); }
912
- .whyx-step .whyx-field, .whyx-step .whyx-field:hover { border: none !important; box-shadow: none !important; transform: none !important; }
913
- .whyx-step .whyx-field:focus-within { box-shadow: 0 0 0 2px rgba(79,70,229,0.14) !important; }
914
-
915
  .whyx-field { border: 1px solid var(--input-border) !important; border-radius: var(--radius-md) !important; box-shadow: var(--shadow-sm) !important; transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition) !important; }
916
  .whyx-field:hover { border-color: var(--accent-border) !important; box-shadow: 0 0 0 3px var(--accent-glow), var(--shadow-sm) !important; transform: translateY(-1px); }
917
  .whyx-field textarea, .whyx-field input[type="text"] { border: 1px solid var(--input-border) !important; border-radius: var(--radius-md) !important; box-shadow: var(--shadow-sm) !important; transition: border-color var(--transition), box-shadow var(--transition) !important; }
@@ -979,7 +1003,6 @@ input[type="range"]::-moz-range-thumb { width: 16px !important; height: 16px !im
979
  .whyx-theme-btn label.selected { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; color: var(--text-accent) !important; }
980
 
981
  .whyx-info-text, .gradio-container .info { color: var(--text-dim) !important; font-size: 11px !important; line-height: 1.45 !important; border-left: 2px solid rgba(110,231,210,0.35) !important; background: var(--chip-bg) !important; padding: 6px 10px !important; border-radius: 0 var(--radius-sm) var(--radius-sm) 0 !important; margin-top: 4px !important; }
982
- .whyx-info-highlight { color: var(--text-accent) !important; font-weight: 600 !important; }
983
  .whyx-tooltip { position: relative !important; cursor: pointer !important; }
984
  .whyx-tooltip:hover .whyx-tooltip-text, .whyx-tooltip:focus-within .whyx-tooltip-text { visibility: visible !important; opacity: 1 !important; }
985
  .whyx-tooltip-text { visibility: hidden !important; opacity: 0 !important; position: absolute !important; z-index: 9999 !important; padding: 4px 8px !important; background: var(--surface) !important; color: var(--text) !important; font-size: 10px !important; font-weight: 600 !important; border-radius: var(--radius-sm) !important; border: 1px solid var(--glass-border) !important; white-space: nowrap !important; pointer-events: none !important; transition: opacity 0.15s ease !important; box-shadow: var(--shadow-md) !important; border-left: 2px solid rgba(110,231,210,0.35) !important; bottom: 110% !important; left: 50% !important; transform: translateX(-50%) !important; }
@@ -992,7 +1015,6 @@ input[type="range"]::-moz-range-thumb { width: 16px !important; height: 16px !im
992
  .whyx-tagger-apply-btn button:hover { background: linear-gradient(135deg, #9D7CFA, #5B6BFA, #3B8BE0) !important; box-shadow: 0 6px 20px rgba(79,70,229,0.35) !important; transform: translateY(-1px) !important; }
993
  .whyx-tagger-copy-btn button { background: var(--chip-bg) !important; border: 1px solid var(--chip-border) !important; color: var(--chip-text) !important; font-weight: 600 !important; box-shadow: none !important; }
994
  .whyx-tagger-copy-btn button:hover:not(:disabled) { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; color: var(--text-accent) !important; box-shadow: 0 4px 14px rgba(79,70,229,0.15) !important; }
995
- .whyx-tagger-tags-card { border-radius: var(--radius-lg) !important; border: 1px solid rgba(129,140,248,0.14) !important; box-shadow: var(--shadow-md) !important; background: var(--surface) !important; }
996
  .whyx-tagger-tags-box textarea { font-family: 'JetBrains Mono', ui-monospace, monospace !important; font-size: 12px !important; line-height: 1.5 !important; color: var(--input-text) !important; background: var(--input-bg) !important; border-radius: var(--radius-md) !important; }
997
  .whyx-tagger-chips .wrap, .whyx-tagger-chips .grid { display: flex !important; flex-wrap: wrap !important; gap: 6px !important; max-height: 260px !important; overflow-y: auto !important; padding: 4px 2px 8px !important; }
998
  .whyx-tagger-chips label { display: inline-flex !important; align-items: center !important; gap: 6px !important; border: 1px solid var(--chip-border) !important; background: var(--chip-bg) !important; border-radius: var(--radius-pill) !important; padding: 5px 12px !important; font-size: 12px !important; font-weight: 500 !important; color: var(--chip-text) !important; cursor: pointer !important; transition: all var(--transition-fast) !important; user-select: none !important; }
@@ -1001,16 +1023,18 @@ input[type="range"]::-moz-range-thumb { width: 16px !important; height: 16px !im
1001
  .whyx-tagger-chips label:has(input:checked) { background: var(--accent-soft) !important; border-color: rgba(110,231,210,0.55) !important; color: #E6FBF5 !important; box-shadow: 0 0 10px rgba(79,70,229,0.18) !important; }
1002
  .whyx-tagger-panel { background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-md) !important; padding: 12px 16px !important; margin-top: 4px !important; animation: whyxFadeIn 0.3s ease both !important; }
1003
  .whyx-tagger-panel-title { font-size: 12px !important; font-weight: 600 !important; color: var(--text) !important; margin-bottom: 6px !important; }
1004
- .whyx-tagger-rating-row { font-size: 11px !important; color: var(--text-dim) !important; margin-bottom: 2px !important; }
1005
- .whyx-tagger-rating-name { width: 92px !important; display: inline-block !important; font-weight: 600 !important; }
1006
- .whyx-tagger-rating-bar { font-family: monospace !important; letter-spacing: -1px !important; }
1007
- .whyx-tagger-rating-pct { color: var(--text-faint) !important; }
 
1008
  .whyx-tagger-badges { display: flex !important; flex-wrap: wrap !important; gap: 6px !important; }
1009
  .whyx-tagger-char-badge { display: inline-block !important; background: linear-gradient(135deg, rgba(129,140,248,0.18), rgba(99,102,241,0.14)) !important; border: 1px solid rgba(129,140,248,0.3) !important; border-radius: var(--radius-pill) !important; padding: 3px 10px !important; font-size: 12px !important; color: #E0E7FF !important; }
1010
  .whyx-tagger-badge-pct { color: #A5B4FC !important; font-size: 10px !important; margin-left: 2px !important; }
1011
  .whyx-tagger-gen-list { display: flex !important; flex-direction: column !important; gap: 1px !important; }
1012
  .whyx-tagger-gen-row { display: flex !important; align-items: center !important; gap: 8px !important; font-size: 11px !important; color: var(--text-dim) !important; white-space: nowrap !important; }
1013
- .whyx-tagger-gen-bar { color: #5AC8F5 !important; font-family: monospace !important; width: 90px !important; text-align: right !important; flex-shrink: 0 !important; overflow: hidden !important; }
 
1014
  .whyx-tagger-gen-name { overflow: hidden !important; text-overflow: ellipsis !important; }
1015
  .whyx-tagger-pose-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
1016
  .whyx-tagger-pose-people { font-size: 11px !important; color: var(--text) !important; font-weight: 600 !important; }
@@ -1018,21 +1042,29 @@ input[type="range"]::-moz-range-thumb { width: 16px !important; height: 16px !im
1018
  .whyx-tagger-anatomy-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
1019
  .whyx-tagger-expr-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
1020
  .whyx-tagger-bg-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1021
 
1022
  .progress-level .progress-bar { background: var(--accent) !important; background-size: 200% auto !important; animation: progressShimmer 2s linear infinite !important; }
1023
  .progress-level .progress-text { color: var(--text) !important; font-size: 12px !important; }
1024
  .progress-level { background: rgba(15,23,42,0.5) !important; border: 1px solid rgba(124,58,237,0.12) !important; border-radius: var(--radius-md) !important; padding: 8px 12px !important; }
1025
 
1026
- .whyx-accordion-group { gap: 8px !important; }
1027
- .whyx-accordion-group > .gradio-accordion { flex: 1 !important; min-width: 0 !important; }
1028
- .whyx-accordion-group .gradio-accordion { background: var(--surface) !important; border: 1px solid var(--glass-border) !important; border-radius: var(--radius-md) !important; }
1029
- .whyx-accordion-group .gradio-accordion label[class*="accordion"],
1030
- .whyx-accordion-group .gradio-accordion button,
1031
- .whyx-accordion-group .gradio-accordion summary {
1032
- color: var(--text) !important; font-weight: 600 !important; font-size: 12px !important; padding: 8px 12px !important; border-bottom: 1px solid var(--glass-border) !important; cursor: pointer !important; background: transparent !important;
1033
- }
1034
- .whyx-accordion-group .gradio-accordion > div:not(label) { padding: 8px 12px !important; }
1035
- .whyx-section-card, .gradio-dropdown, .gradio-dropdown > div, .tabs, .tabitem, [data-testid="tab-item"] { overflow: visible !important; }
1036
 
1037
  /* --- Dropdown menu styling — opaque panel, themed --- */
1038
  [role="listbox"], ul.options, ul.dropdown-options, [data-testid="dropdown-options"] {
@@ -1107,7 +1139,6 @@ ul.options::-webkit-scrollbar-thumb:hover { background: rgba(79,70,229,0.4) !imp
1107
  .whyx-artist-preview-empty { color: var(--text-dim) !important; font-size: 12px !important; font-style: italic !important; }
1108
  .whyx-artist-link { display: inline-flex !important; align-items: center !important; gap: 4px !important; border: 1px solid var(--chip-border) !important; border-radius: var(--radius-sm) !important; padding: 4px 10px !important; background: var(--chip-bg) !important; color: var(--chip-text) !important; text-decoration: none !important; font-size: 11px !important; font-weight: 600 !important; transition: all var(--transition-fast) !important; }
1109
  .whyx-artist-link:hover { background: var(--accent-glow) !important; border-color: rgba(124,58,237,0.25) !important; box-shadow: 0 2px 8px rgba(79,70,229,0.08) !important; color: var(--text-accent) !important; }
1110
- .whyx-tag-bar { width: 80px !important; display: inline-block !important; white-space: nowrap !important; font-family: monospace !important; }
1111
 
1112
  @media (max-width: 767px) {
1113
  .whyx-title { font-size: 24px !important; } .whyx-section-title { font-size: 11px !important; }
@@ -1125,6 +1156,14 @@ ul.options::-webkit-scrollbar-thumb:hover { background: rgba(79,70,229,0.4) !imp
1125
  .tabs button { flex-shrink: 0 !important; font-size: 11px !important; padding: 6px 10px !important; }
1126
  .whyx-tooltip-text { bottom: auto !important; top: 110% !important; }
1127
  .whyx-tooltip-text::after { top: -8px !important; border-color: transparent transparent var(--surface) transparent !important; }
 
 
 
 
 
 
 
 
1128
  }
1129
  @media (min-width: 768px) and (max-width: 1023px) {
1130
  .whyx-title { font-size: 26px !important; }
@@ -1212,72 +1251,6 @@ ul.options::-webkit-scrollbar-thumb:hover { background: rgba(79,70,229,0.4) !imp
1212
  to { opacity: 1; transform: translateY(0); }
1213
  }
1214
 
1215
- .whyx-tree-leaf {
1216
- padding: 10px 14px !important;
1217
- border-top: 1px solid var(--tree-node-border) !important;
1218
- transition: background var(--tree-transition) !important;
1219
- }
1220
-
1221
- .whyx-tree-leaf:hover {
1222
- background: var(--tree-leaf-hover) !important;
1223
- }
1224
-
1225
- .whyx-tree-leaf:first-child {
1226
- border-top: none !important;
1227
- }
1228
-
1229
- .whyx-tree-indent {
1230
- margin-left: var(--tree-indent) !important;
1231
- }
1232
-
1233
- .whyx-tree-connector {
1234
- position: absolute !important;
1235
- left: -8px !important;
1236
- top: 0 !important;
1237
- bottom: 0 !important;
1238
- width: 2px !important;
1239
- background: var(--tree-connector) !important;
1240
- border-radius: 1px !important;
1241
- }
1242
-
1243
- .whyx-tree-connector:hover {
1244
- background: var(--tree-connector-hover) !important;
1245
- }
1246
-
1247
- .whyx-tree-item {
1248
- display: flex !important;
1249
- align-items: center !important;
1250
- gap: 8px !important;
1251
- padding: 6px 0 !important;
1252
- }
1253
-
1254
- .whyx-tree-item-icon {
1255
- font-size: 14px !important;
1256
- line-height: 1 !important;
1257
- flex-shrink: 0 !important;
1258
- color: var(--text-accent) !important;
1259
- }
1260
-
1261
- .whyx-tree-item-label {
1262
- color: var(--text) !important;
1263
- font-size: 12px !important;
1264
- font-weight: 500 !important;
1265
- flex: 1 !important;
1266
- }
1267
-
1268
- .whyx-tree-item-value {
1269
- color: var(--text-dim) !important;
1270
- font-size: 11px !important;
1271
- font-family: 'JetBrains Mono', ui-monospace, monospace !important;
1272
- }
1273
-
1274
- .whyx-tree-divider {
1275
- height: 1px !important;
1276
- background: linear-gradient(90deg, transparent, var(--tree-connector), transparent) !important;
1277
- margin: 8px 0 !important;
1278
- border: none !important;
1279
- }
1280
-
1281
  /* Tree node header (replaces _section for tree) */
1282
  .whyx-tree-header {
1283
  display: flex !important;
@@ -1451,7 +1424,8 @@ with gr.Blocks(**_blocks_kw) as demo:
1451
 
1452
  with gr.Tabs():
1453
  # ===== TAB 1: Generate =====
1454
- with gr.TabItem(f"✨ {t('tab_generate', 'en')}"):
 
1455
  with gr.Column(elem_classes=["whyx-tree"]):
1456
  # Tree root: Prompt
1457
  with gr.Column(elem_classes=["whyx-tree-node"]):
@@ -1711,11 +1685,14 @@ with gr.Blocks(**_blocks_kw) as demo:
1711
  result_boxes = []
1712
  neg_boxes = []
1713
  heart_btns = []
 
 
1714
  for i in range(MAX_OUTPUTS):
1715
- with gr.Row(elem_classes=["whyx-result-card"]) as card:
1716
- with gr.Column(scale=0, min_width=40):
 
1717
  gr.HTML(f'<div class="whyx-result-num">#{i+1}</div>')
1718
- with gr.Column(scale=1):
1719
  rb = gr.Textbox(
1720
  label="",
1721
  show_label=False,
@@ -1725,7 +1702,7 @@ with gr.Blocks(**_blocks_kw) as demo:
1725
  placeholder=t("result_placeholder", "en"),
1726
  elem_classes=["whyx-result-text"],
1727
  )
1728
- with gr.Column(scale=0, min_width=36):
1729
  cb_pos = gr.Button(
1730
  "📋",
1731
  size="sm",
@@ -1733,11 +1710,12 @@ with gr.Blocks(**_blocks_kw) as demo:
1733
  scale=0,
1734
  min_width=32,
1735
  elem_classes=["whyx-copy-btn", "whyx-tooltip"],
1736
- elem_id="copy-btn-pos",
1737
  )
1738
  cb_pos.click(fn=None, inputs=[rb], outputs=[], js=COPY_JS)
1739
- gr.HTML('<span class="whyx-tooltip-text">Copy to clipboard</span>')
1740
- with gr.Column(scale=0, min_width=46):
 
1741
  hb = gr.Button(
1742
  "♡",
1743
  size="sm",
@@ -1745,11 +1723,12 @@ with gr.Blocks(**_blocks_kw) as demo:
1745
  scale=0,
1746
  min_width=36,
1747
  elem_classes=["whyx-heart-btn", "whyx-tooltip"],
1748
- elem_id="heart-btn",
1749
  )
1750
  heart_btns.append(hb)
1751
- gr.HTML('<span class="whyx-tooltip-text">Toggle favorite</span>')
1752
- with gr.Column(scale=1):
 
1753
  nb = gr.Textbox(
1754
  label="",
1755
  show_label=False,
@@ -1759,7 +1738,7 @@ with gr.Blocks(**_blocks_kw) as demo:
1759
  placeholder=t("neg_placeholder", "en"),
1760
  elem_classes=["whyx-neg-text"],
1761
  )
1762
- with gr.Column(scale=0, min_width=36):
1763
  cb_neg = gr.Button(
1764
  "📋",
1765
  size="sm",
@@ -1767,15 +1746,17 @@ with gr.Blocks(**_blocks_kw) as demo:
1767
  scale=0,
1768
  min_width=32,
1769
  elem_classes=["whyx-copy-btn", "whyx-tooltip"],
1770
- elem_id="copy-btn-neg",
1771
  )
1772
  cb_neg.click(fn=None, inputs=[nb], outputs=[], js=COPY_JS)
1773
- gr.HTML('<span class="whyx-tooltip-text">Copy to clipboard</span>')
 
1774
  result_boxes.append(rb)
1775
  neg_boxes.append(nb)
1776
 
1777
  # ===== TAB 2: Tagger =====
1778
- with gr.TabItem(f"🖼️ {t('tab_tagger', 'en')}"):
 
1779
  with gr.Column(elem_classes=["whyx-tree"]):
1780
  # Tree root: Tagger
1781
  with gr.Column(elem_classes=["whyx-tree-node"]):
@@ -1912,7 +1893,8 @@ with gr.Blocks(**_blocks_kw) as demo:
1912
  tagger_esc_state = gr.State("")
1913
 
1914
  # ===== TAB 3: Tools =====
1915
- with gr.TabItem(f"🔧 {t('tab_tools', 'en')}"):
 
1916
  with gr.Column(elem_classes=["whyx-tree"]):
1917
  # Tree root: Analyzer
1918
  with gr.Column(elem_classes=["whyx-tree-node"]):
@@ -2025,7 +2007,8 @@ with gr.Blocks(**_blocks_kw) as demo:
2025
  )
2026
 
2027
  # ===== TAB 4: History =====
2028
- with gr.TabItem(f"📜 {t('tab_history', 'en')}"):
 
2029
  with gr.Column(elem_classes=["whyx-tree-node"]):
2030
  history_header_html = gr.HTML(_tree_header("📜", "history_section", "en"))
2031
  history_md = gr.HTML(
@@ -2049,7 +2032,7 @@ with gr.Blocks(**_blocks_kw) as demo:
2049
  outputs=[],
2050
  )
2051
  web_search_clear.click(
2052
- fn=lambda lang: f'<div class="whyx-info-text" style="color:rgba(148,163,184,0.6);font-size:12px;">{t("web_search_empty", "ru" if lang == "RU" else "en")}</div>',
2053
  inputs=[lang_state],
2054
  outputs=[web_search_output],
2055
  )
@@ -2206,7 +2189,7 @@ with gr.Blocks(**_blocks_kw) as demo:
2206
  generate_btn.click(
2207
  fn=on_generate,
2208
  inputs=inputs_list,
2209
- outputs=result_boxes + neg_boxes,
2210
  ).then(
2211
  fn=lambda lang: gr.update(value=_format_history_html("ru" if lang == "RU" else "en")),
2212
  inputs=[lang_state],
@@ -2400,6 +2383,13 @@ with gr.Blocks(**_blocks_kw) as demo:
2400
  updates.append(gr.update(value=_format_artist_info([], lc)))
2401
  updates.append(gr.update(value=f'<div class="whyx-info-text">{t("analyzer_desc", lc)}</div>'))
2402
  updates.append(gr.update(value=_format_history_html(lc)))
 
 
 
 
 
 
 
2403
  updates.append(choice)
2404
  return updates
2405
 
@@ -2437,19 +2427,44 @@ with gr.Blocks(**_blocks_kw) as demo:
2437
  presets_acc, user_preset_name, user_preset_tags, user_preset_save, user_preset_delete,
2438
  user_preset_trigger, user_preset_dropdown, user_preset_apply, theme_toggle,
2439
  ] + category_checks + group_components + result_boxes + neg_boxes + [categories_desc_md, presets_desc_md, results_desc_md, hotkeys_hint_md, artist_info_md, analyzer_md, history_md,
2440
- lang_state]
2441
 
2442
  lang_btn.change(
2443
  fn=update_lang,
2444
  inputs=[lang_btn],
2445
  outputs=lang_outputs,
2446
  )
 
 
 
 
 
 
2447
 
2448
  theme_toggle.change(
2449
  fn=None,
2450
  inputs=[theme_toggle],
2451
  outputs=[],
2452
- js="(v) => { document.body.classList.toggle('whyx-light', v === 'light'); }",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2453
  )
2454
 
2455
 
 
105
  }"""
106
 
107
 
108
+ def on_generate(prompt, model, rating, num_variations, creativity, weight_mode, mode, lang, artist_style, selected_artists, use_tandems, selected_tandem_idx, web_enrich, seed, current_preset_state, fx_count, blacklist_text, mirror_blacklist, strip_quality, strip_artist, strip_lora, strip_meta, min_tags, user_preset_state, output_format, *checks) -> tuple:
109
+ def _card_updates(positives):
110
+ return tuple(
111
+ gr.update(visible=bool((positives[i] or "").strip()))
112
+ for i in range(MAX_OUTPUTS)
113
+ )
114
+
115
  try:
116
  if not prompt or not prompt.strip():
117
+ return tuple([""] * (MAX_OUTPUTS * 2)) + _card_updates([""] * MAX_OUTPUTS)
118
  parsed = parse_prompt(prompt)
119
  if parsed is None:
120
  msg = t("error_parse", lang)
121
+ outs = [msg] + [""] * (MAX_OUTPUTS - 1)
122
+ return tuple(outs) + tuple([""] * MAX_OUTPUTS) + _card_updates(outs)
123
  active = [c for c, on in zip(ALL_CATEGORIES, checks) if on]
124
  if not active:
125
  active = ["quality", "lighting", "expression"]
 
198
  categories=active,
199
  seed=int(seed) if seed is not None and str(seed).strip() else None,
200
  )
201
+ return tuple(out[:MAX_OUTPUTS]) + tuple(neg_out[:MAX_OUTPUTS]) + _card_updates(out[:MAX_OUTPUTS])
202
  except Exception as exc:
203
  import traceback
204
  traceback.print_exc()
205
  err_msg = t("generation_error", lang).format(exc=html.escape(str(exc)))
206
+ outs = [err_msg] + [""] * (MAX_OUTPUTS - 1)
207
+ return tuple(outs) + tuple([""] * MAX_OUTPUTS) + _card_updates(outs)
208
 
209
 
210
  def on_suggest(prompt, model, rating, lang) -> str:
 
270
  from src.tag_searcher import search_tags
271
  lc = "ru" if lang == "RU" else "en"
272
  if not query or not query.strip():
273
+ return f'<div class="whyx-info-text" style="color:var(--text-faint);font-size:12px;">{t("web_search_empty", lc)}</div>'
274
  try:
275
  results = search_tags(query, warehouse)
276
  parts = []
277
  if results["local"]:
278
+ parts.append(f'<div style="margin-top:6px;font-size:13px;font-weight:600;color:var(--text);">🔍 {t("web_search_offline", lc)} ({len(results["local"])})</div>')
279
  for r in results["local"][:15]:
280
  safe_tag = html.escape(r["tag"])
281
  safe_cat = html.escape(r["category"])
282
+ parts.append(f'<span style="display:inline-block;background:var(--accent-glow);border:1px solid var(--accent-border);border-radius:4px;padding:1px 6px;margin:2px;font-size:12px;color:var(--text);">{safe_tag} <span style="color:var(--text-faint);font-size:10px;">[{safe_cat}]</span></span>')
283
  if results["web"]:
284
+ parts.append(f'<div style="margin-top:8px;font-size:13px;font-weight:600;color:var(--text);">🌐 {t("web_search_online", lc)} ({len(results["web"])})</div>')
285
  for r in results["web"][:MAX_OUTPUTS]:
286
  safe_tag = html.escape(r.get("tag", ""))
287
  count = r.get("post_count", 0)
288
+ parts.append(f'<span style="display:inline-block;background:var(--accent-glow);border:1px solid var(--accent-border);border-radius:4px;padding:1px 6px;margin:2px;font-size:12px;color:var(--text);">{safe_tag} <span style="color:var(--text-faint);font-size:10px;">({count} posts)</span></span>')
289
  if not parts:
290
  return f'<div class="whyx-info-text" style="font-size:12px;">{t("web_enrich_no_results", lc)}</div>'
291
  return '<div style="line-height:1.8;">' + "".join(parts) + '</div>'
 
297
  lc = "ru" if lang == "RU" else "en"
298
  history = get_history()
299
  lines = []
300
+ lines.append("<div style='background:var(--input-bg);border:1px solid rgba(124,58,237,0.12);border-radius:10px;padding:10px 14px;margin-top:4px;'>")
301
 
302
  entries = history.get_all()[:5]
303
  if not entries:
 
498
  nl_caption = (result.get("nl_caption") or "").strip()
499
  if nl_caption:
500
  lines.append(
501
+ f"<div class='whyx-tagger-panel-title'>{t('tagger_nl_caption', lc)}</div>"
502
  f"<div class='whyx-tagger-nl-caption'>{html.escape(nl_caption)}</div>"
503
  )
504
 
 
542
  for name, score in result["ratings"].items():
543
  color = rating_colors.get(name, "var(--text-dim)")
544
  pct = int(score * 100)
545
+ lines.append(
546
+ f"<div class='whyx-tagger-rating-row'>"
547
+ f"<span class='whyx-tagger-rating-name' style='color:{color};'>{name}</span>"
548
+ f"<span class='whyx-tagger-rating-track'><span class='whyx-tagger-rating-fill' style='width:{pct}%;background:{color};'></span></span>"
549
+ f"<span class='whyx-tagger-rating-pct'>({score:.1%})</span></div>"
550
+ )
551
 
552
  if result["characters"]:
553
  lines.append(f"<div class='whyx-tagger-panel-title' style='margin-top:10px;'>{t('tagger_characters', lc)}</div>")
 
561
  for name, score in list(result["general"].items())[:14]:
562
  safe = html.escape(name.replace("_", " ").replace("(", "\\(").replace(")", "\\)"))
563
  pct = int(score * 100)
564
+ lines.append(
565
+ f"<div class='whyx-tagger-gen-row'>"
566
+ f"<span class='whyx-tagger-gen-track'><span class='whyx-tagger-gen-fill' style='width:{pct}%;'></span></span>"
567
+ f"<span class='whyx-tagger-gen-name'>{safe}</span>"
568
+ f"<span class='whyx-tagger-badge-pct'>{score:.0%}</span></div>"
569
+ )
570
  lines.append("</div>")
571
  lines.append("</div>")
572
  return "\n".join(lines)
 
900
  --button-secondary-background-fill: transparent !important; --button-secondary-background-fill-hover: rgba(15,23,42,0.05) !important;
901
  --button-secondary-border-color: rgba(15,23,42,0.16) !important;
902
  }
903
+ /* --- Light theme component overrides --- */
904
+ body.whyx-light {
905
+ --tree-node-bg: rgba(255,255,255,0.92);
906
+ --tree-node-border: rgba(15,23,42,0.08);
907
+ --tree-node-border-hover: rgba(15,23,42,0.16);
908
+ --tree-branch-hover: rgba(79,70,229,0.06);
909
+ --tree-leaf-hover: rgba(241,245,249,0.9);
910
+ }
911
+ body.whyx-light::before { opacity: 0.45 !important; }
912
+ body.whyx-light [role="listbox"], body.whyx-light ul.options, body.whyx-light ul.dropdown-options, body.whyx-light [data-testid="dropdown-options"] {
913
+ background: rgba(255,255,255,0.98) !important;
914
+ border: 1px solid rgba(15,23,42,0.12) !important;
915
+ box-shadow: 0 8px 32px rgba(15,23,42,0.16), 0 0 0 1px rgba(79,70,229,0.05) !important;
916
+ }
917
+ body.whyx-light .gradio-dropdown { background: #FFFFFF !important; }
918
+ body.whyx-light .whyx-tagger-char-badge { background: linear-gradient(135deg, rgba(129,140,248,0.14), rgba(99,102,241,0.1)) !important; border-color: rgba(99,102,241,0.35) !important; color: #3730A3 !important; }
919
+ body.whyx-light .whyx-tagger-badge-pct { color: #6366F1 !important; }
920
+ body.whyx-light .progress-level { background: rgba(255,255,255,0.85) !important; border-color: rgba(124,58,237,0.18) !important; }
921
+ body.whyx-light .whyx-tagger-chips label:has(input:checked) { color: #0F766E !important; }
922
+ body.whyx-light .whyx-slide-trigger { background: linear-gradient(135deg, rgba(255,255,255,0.85), rgba(241,245,249,0.65)) !important; border-color: rgba(15,23,42,0.12) !important; }
923
+ body.whyx-light *::-webkit-scrollbar-track { background: rgba(15,23,42,0.05) !important; }
924
+ body.whyx-light *::-webkit-scrollbar-thumb { background: rgba(15,23,42,0.18) !important; }
925
+ body.whyx-light *::-webkit-scrollbar-thumb:hover { background: rgba(15,23,42,0.3) !important; }
926
  @keyframes whyxBreath { 0%, 100% { opacity: 0.65; } 50% { opacity: 1; } }
927
  @keyframes whyxFadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
928
  @keyframes progressShimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
 
932
  :focus-visible { outline: 2px solid rgba(90,200,245,0.6) !important; outline-offset: 2px !important; }
933
  *::-webkit-scrollbar { width: 5px; height: 5px; } *::-webkit-scrollbar-track { background: rgba(15,23,42,0.25); } *::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 3px; } *::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.14); }
934
 
 
 
 
 
 
 
935
  .whyx-title { background: linear-gradient(135deg, #7C3AED, #4F46E5 38%, #2563EB 70%, #10B981) !important; -webkit-background-clip: text !important; -webkit-text-fill-color: transparent !important; background-clip: text !important; color: transparent !important; display: inline-block !important; font-weight: 800 !important; font-size: 32px !important; letter-spacing: -0.6px !important; line-height: 1.2 !important; }
936
  .whyx-subtitle { font-size: 12px !important; color: var(--text-dim) !important; margin-top: 2px !important; border-left: 2px solid rgba(110,231,210,0.45) !important; padding-left: 8px !important; }
937
  .whyx-divider { height: 1px !important; background: linear-gradient(90deg, transparent, rgba(110,231,210,0.2), rgba(176,124,240,0.2), transparent) !important; margin: 10px 0 !important; border: none !important; }
938
 
 
 
 
 
 
 
 
 
 
939
  .whyx-field { border: 1px solid var(--input-border) !important; border-radius: var(--radius-md) !important; box-shadow: var(--shadow-sm) !important; transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition) !important; }
940
  .whyx-field:hover { border-color: var(--accent-border) !important; box-shadow: 0 0 0 3px var(--accent-glow), var(--shadow-sm) !important; transform: translateY(-1px); }
941
  .whyx-field textarea, .whyx-field input[type="text"] { border: 1px solid var(--input-border) !important; border-radius: var(--radius-md) !important; box-shadow: var(--shadow-sm) !important; transition: border-color var(--transition), box-shadow var(--transition) !important; }
 
1003
  .whyx-theme-btn label.selected { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; color: var(--text-accent) !important; }
1004
 
1005
  .whyx-info-text, .gradio-container .info { color: var(--text-dim) !important; font-size: 11px !important; line-height: 1.45 !important; border-left: 2px solid rgba(110,231,210,0.35) !important; background: var(--chip-bg) !important; padding: 6px 10px !important; border-radius: 0 var(--radius-sm) var(--radius-sm) 0 !important; margin-top: 4px !important; }
 
1006
  .whyx-tooltip { position: relative !important; cursor: pointer !important; }
1007
  .whyx-tooltip:hover .whyx-tooltip-text, .whyx-tooltip:focus-within .whyx-tooltip-text { visibility: visible !important; opacity: 1 !important; }
1008
  .whyx-tooltip-text { visibility: hidden !important; opacity: 0 !important; position: absolute !important; z-index: 9999 !important; padding: 4px 8px !important; background: var(--surface) !important; color: var(--text) !important; font-size: 10px !important; font-weight: 600 !important; border-radius: var(--radius-sm) !important; border: 1px solid var(--glass-border) !important; white-space: nowrap !important; pointer-events: none !important; transition: opacity 0.15s ease !important; box-shadow: var(--shadow-md) !important; border-left: 2px solid rgba(110,231,210,0.35) !important; bottom: 110% !important; left: 50% !important; transform: translateX(-50%) !important; }
 
1015
  .whyx-tagger-apply-btn button:hover { background: linear-gradient(135deg, #9D7CFA, #5B6BFA, #3B8BE0) !important; box-shadow: 0 6px 20px rgba(79,70,229,0.35) !important; transform: translateY(-1px) !important; }
1016
  .whyx-tagger-copy-btn button { background: var(--chip-bg) !important; border: 1px solid var(--chip-border) !important; color: var(--chip-text) !important; font-weight: 600 !important; box-shadow: none !important; }
1017
  .whyx-tagger-copy-btn button:hover:not(:disabled) { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; color: var(--text-accent) !important; box-shadow: 0 4px 14px rgba(79,70,229,0.15) !important; }
 
1018
  .whyx-tagger-tags-box textarea { font-family: 'JetBrains Mono', ui-monospace, monospace !important; font-size: 12px !important; line-height: 1.5 !important; color: var(--input-text) !important; background: var(--input-bg) !important; border-radius: var(--radius-md) !important; }
1019
  .whyx-tagger-chips .wrap, .whyx-tagger-chips .grid { display: flex !important; flex-wrap: wrap !important; gap: 6px !important; max-height: 260px !important; overflow-y: auto !important; padding: 4px 2px 8px !important; }
1020
  .whyx-tagger-chips label { display: inline-flex !important; align-items: center !important; gap: 6px !important; border: 1px solid var(--chip-border) !important; background: var(--chip-bg) !important; border-radius: var(--radius-pill) !important; padding: 5px 12px !important; font-size: 12px !important; font-weight: 500 !important; color: var(--chip-text) !important; cursor: pointer !important; transition: all var(--transition-fast) !important; user-select: none !important; }
 
1023
  .whyx-tagger-chips label:has(input:checked) { background: var(--accent-soft) !important; border-color: rgba(110,231,210,0.55) !important; color: #E6FBF5 !important; box-shadow: 0 0 10px rgba(79,70,229,0.18) !important; }
1024
  .whyx-tagger-panel { background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-md) !important; padding: 12px 16px !important; margin-top: 4px !important; animation: whyxFadeIn 0.3s ease both !important; }
1025
  .whyx-tagger-panel-title { font-size: 12px !important; font-weight: 600 !important; color: var(--text) !important; margin-bottom: 6px !important; }
1026
+ .whyx-tagger-rating-row { font-size: 11px !important; color: var(--text-dim) !important; margin-bottom: 4px !important; display: flex !important; align-items: center !important; gap: 8px !important; }
1027
+ .whyx-tagger-rating-name { width: 92px !important; display: inline-block !important; font-weight: 600 !important; flex-shrink: 0 !important; }
1028
+ .whyx-tagger-rating-track { flex: 1 !important; height: 6px !important; background: var(--chip-bg) !important; border: 1px solid var(--chip-border) !important; border-radius: var(--radius-pill) !important; overflow: hidden !important; }
1029
+ .whyx-tagger-rating-fill { display: block !important; height: 100% !important; border-radius: var(--radius-pill) !important; transition: width 0.4s ease !important; }
1030
+ .whyx-tagger-rating-pct { color: var(--text-faint) !important; flex-shrink: 0 !important; }
1031
  .whyx-tagger-badges { display: flex !important; flex-wrap: wrap !important; gap: 6px !important; }
1032
  .whyx-tagger-char-badge { display: inline-block !important; background: linear-gradient(135deg, rgba(129,140,248,0.18), rgba(99,102,241,0.14)) !important; border: 1px solid rgba(129,140,248,0.3) !important; border-radius: var(--radius-pill) !important; padding: 3px 10px !important; font-size: 12px !important; color: #E0E7FF !important; }
1033
  .whyx-tagger-badge-pct { color: #A5B4FC !important; font-size: 10px !important; margin-left: 2px !important; }
1034
  .whyx-tagger-gen-list { display: flex !important; flex-direction: column !important; gap: 1px !important; }
1035
  .whyx-tagger-gen-row { display: flex !important; align-items: center !important; gap: 8px !important; font-size: 11px !important; color: var(--text-dim) !important; white-space: nowrap !important; }
1036
+ .whyx-tagger-gen-track { width: 90px !important; height: 5px !important; background: var(--chip-bg) !important; border: 1px solid var(--chip-border) !important; border-radius: var(--radius-pill) !important; overflow: hidden !important; flex-shrink: 0 !important; }
1037
+ .whyx-tagger-gen-fill { display: block !important; height: 100% !important; background: linear-gradient(90deg, #5AC8F5, #38BDF8) !important; border-radius: var(--radius-pill) !important; transition: width 0.4s ease !important; }
1038
  .whyx-tagger-gen-name { overflow: hidden !important; text-overflow: ellipsis !important; }
1039
  .whyx-tagger-pose-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
1040
  .whyx-tagger-pose-people { font-size: 11px !important; color: var(--text) !important; font-weight: 600 !important; }
 
1042
  .whyx-tagger-anatomy-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
1043
  .whyx-tagger-expr-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
1044
  .whyx-tagger-bg-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
1045
+ .whyx-tagger-seg-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(16,185,129,0.16) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
1046
+ .whyx-tagger-depth-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(37,99,235,0.18) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
1047
+ .whyx-tagger-control-img { border-radius: var(--radius-md) !important; overflow: hidden !important; border: 1px solid var(--glass-border) !important; }
1048
+ .whyx-tagger-control-img img { border-radius: var(--radius-md) !important; }
1049
+
1050
+ .whyx-suggest-box { background: var(--input-bg) !important; border: 1px solid rgba(124,58,237,0.14) !important; border-radius: var(--radius-md) !important; padding: 10px 14px !important; margin-top: 6px !important; animation: whyxFadeIn 0.3s ease both !important; }
1051
+ .whyx-suggest-title { font-size: 12px !important; font-weight: 600 !important; color: var(--text) !important; margin-bottom: 6px !important; }
1052
+ .whyx-suggest-box ul { margin: 0 !important; padding-left: 18px !important; }
1053
+ .whyx-suggest-item { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.5 !important; padding: 1px 0 !important; }
1054
+
1055
+ .whyx-chip-group { display: flex !important; flex-wrap: wrap !important; gap: 6px !important; }
1056
+ .whyx-chip-group label { display: inline-flex !important; align-items: center !important; border: 1px solid var(--chip-border) !important; background: var(--chip-bg) !important; border-radius: var(--radius-pill) !important; padding: 5px 12px !important; font-size: 11px !important; cursor: pointer !important; transition: all var(--transition-fast) !important; user-select: none !important; }
1057
+ .whyx-chip-group label:hover { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; }
1058
+ .whyx-chip-group label:has(input:checked) { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; color: var(--text-accent) !important; }
1059
+ .whyx-checkbox label { font-size: 12px !important; color: var(--text-dim) !important; }
1060
+ .whyx-strip-row { gap: 10px !important; flex-wrap: wrap !important; }
1061
+ .whyx-strip-row .gradio-checkbox { min-width: 0 !important; }
1062
 
1063
  .progress-level .progress-bar { background: var(--accent) !important; background-size: 200% auto !important; animation: progressShimmer 2s linear infinite !important; }
1064
  .progress-level .progress-text { color: var(--text) !important; font-size: 12px !important; }
1065
  .progress-level { background: rgba(15,23,42,0.5) !important; border: 1px solid rgba(124,58,237,0.12) !important; border-radius: var(--radius-md) !important; padding: 8px 12px !important; }
1066
 
1067
+ .gradio-dropdown, .gradio-dropdown > div, .tabs, .tabitem, [data-testid="tab-item"] { overflow: visible !important; }
 
 
 
 
 
 
 
 
 
1068
 
1069
  /* --- Dropdown menu styling — opaque panel, themed --- */
1070
  [role="listbox"], ul.options, ul.dropdown-options, [data-testid="dropdown-options"] {
 
1139
  .whyx-artist-preview-empty { color: var(--text-dim) !important; font-size: 12px !important; font-style: italic !important; }
1140
  .whyx-artist-link { display: inline-flex !important; align-items: center !important; gap: 4px !important; border: 1px solid var(--chip-border) !important; border-radius: var(--radius-sm) !important; padding: 4px 10px !important; background: var(--chip-bg) !important; color: var(--chip-text) !important; text-decoration: none !important; font-size: 11px !important; font-weight: 600 !important; transition: all var(--transition-fast) !important; }
1141
  .whyx-artist-link:hover { background: var(--accent-glow) !important; border-color: rgba(124,58,237,0.25) !important; box-shadow: 0 2px 8px rgba(79,70,229,0.08) !important; color: var(--text-accent) !important; }
 
1142
 
1143
  @media (max-width: 767px) {
1144
  .whyx-title { font-size: 24px !important; } .whyx-section-title { font-size: 11px !important; }
 
1156
  .tabs button { flex-shrink: 0 !important; font-size: 11px !important; padding: 6px 10px !important; }
1157
  .whyx-tooltip-text { bottom: auto !important; top: 110% !important; }
1158
  .whyx-tooltip-text::after { top: -8px !important; border-color: transparent transparent var(--surface) transparent !important; }
1159
+ /* Result cards: compact wrapped layout instead of a tall vertical stack */
1160
+ #root .whyx-result-card { flex-direction: row !important; flex-wrap: wrap !important; align-items: flex-start !important; gap: 6px !important; }
1161
+ #root .whyx-result-card > .whyx-rc-num { width: auto !important; flex: 0 0 auto !important; }
1162
+ #root .whyx-result-card > .whyx-rc-pos { width: auto !important; flex: 1 1 60% !important; min-width: 0 !important; }
1163
+ #root .whyx-result-card > .whyx-rc-copypos,
1164
+ #root .whyx-result-card > .whyx-rc-heart { width: auto !important; flex: 0 0 auto !important; }
1165
+ #root .whyx-result-card > .whyx-rc-neg { width: auto !important; flex: 1 1 70% !important; min-width: 0 !important; }
1166
+ #root .whyx-result-card > .whyx-rc-copyneg { width: auto !important; flex: 0 0 auto !important; }
1167
  }
1168
  @media (min-width: 768px) and (max-width: 1023px) {
1169
  .whyx-title { font-size: 26px !important; }
 
1251
  to { opacity: 1; transform: translateY(0); }
1252
  }
1253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1254
  /* Tree node header (replaces _section for tree) */
1255
  .whyx-tree-header {
1256
  display: flex !important;
 
1424
 
1425
  with gr.Tabs():
1426
  # ===== TAB 1: Generate =====
1427
+ tab_generate = gr.TabItem(f"✨ {t('tab_generate', 'en')}")
1428
+ with tab_generate:
1429
  with gr.Column(elem_classes=["whyx-tree"]):
1430
  # Tree root: Prompt
1431
  with gr.Column(elem_classes=["whyx-tree-node"]):
 
1685
  result_boxes = []
1686
  neg_boxes = []
1687
  heart_btns = []
1688
+ result_cards = []
1689
+ tooltip_spans = [] # (component, "copy" | "fav") for i18n updates
1690
  for i in range(MAX_OUTPUTS):
1691
+ with gr.Row(elem_classes=["whyx-result-card"], visible=False) as card:
1692
+ result_cards.append(card)
1693
+ with gr.Column(scale=0, min_width=40, elem_classes=["whyx-rc-num"]):
1694
  gr.HTML(f'<div class="whyx-result-num">#{i+1}</div>')
1695
+ with gr.Column(scale=1, elem_classes=["whyx-rc-pos"]):
1696
  rb = gr.Textbox(
1697
  label="",
1698
  show_label=False,
 
1702
  placeholder=t("result_placeholder", "en"),
1703
  elem_classes=["whyx-result-text"],
1704
  )
1705
+ with gr.Column(scale=0, min_width=36, elem_classes=["whyx-rc-copypos"]):
1706
  cb_pos = gr.Button(
1707
  "📋",
1708
  size="sm",
 
1710
  scale=0,
1711
  min_width=32,
1712
  elem_classes=["whyx-copy-btn", "whyx-tooltip"],
1713
+ elem_id=f"copy-btn-pos-{i}",
1714
  )
1715
  cb_pos.click(fn=None, inputs=[rb], outputs=[], js=COPY_JS)
1716
+ tt_copy_pos = gr.HTML(f'<span class="whyx-tooltip-text">{t("tooltip_copy", "en")}</span>')
1717
+ tooltip_spans.append((tt_copy_pos, "copy"))
1718
+ with gr.Column(scale=0, min_width=46, elem_classes=["whyx-rc-heart"]):
1719
  hb = gr.Button(
1720
  "♡",
1721
  size="sm",
 
1723
  scale=0,
1724
  min_width=36,
1725
  elem_classes=["whyx-heart-btn", "whyx-tooltip"],
1726
+ elem_id=f"heart-btn-{i}",
1727
  )
1728
  heart_btns.append(hb)
1729
+ tt_fav = gr.HTML(f'<span class="whyx-tooltip-text">{t("tooltip_favorite", "en")}</span>')
1730
+ tooltip_spans.append((tt_fav, "fav"))
1731
+ with gr.Column(scale=1, elem_classes=["whyx-rc-neg"]):
1732
  nb = gr.Textbox(
1733
  label="",
1734
  show_label=False,
 
1738
  placeholder=t("neg_placeholder", "en"),
1739
  elem_classes=["whyx-neg-text"],
1740
  )
1741
+ with gr.Column(scale=0, min_width=36, elem_classes=["whyx-rc-copyneg"]):
1742
  cb_neg = gr.Button(
1743
  "📋",
1744
  size="sm",
 
1746
  scale=0,
1747
  min_width=32,
1748
  elem_classes=["whyx-copy-btn", "whyx-tooltip"],
1749
+ elem_id=f"copy-btn-neg-{i}",
1750
  )
1751
  cb_neg.click(fn=None, inputs=[nb], outputs=[], js=COPY_JS)
1752
+ tt_copy_neg = gr.HTML(f'<span class="whyx-tooltip-text">{t("tooltip_copy", "en")}</span>')
1753
+ tooltip_spans.append((tt_copy_neg, "copy"))
1754
  result_boxes.append(rb)
1755
  neg_boxes.append(nb)
1756
 
1757
  # ===== TAB 2: Tagger =====
1758
+ tab_tagger = gr.TabItem(f"🖼️ {t('tab_tagger', 'en')}")
1759
+ with tab_tagger:
1760
  with gr.Column(elem_classes=["whyx-tree"]):
1761
  # Tree root: Tagger
1762
  with gr.Column(elem_classes=["whyx-tree-node"]):
 
1893
  tagger_esc_state = gr.State("")
1894
 
1895
  # ===== TAB 3: Tools =====
1896
+ tab_tools = gr.TabItem(f"🔧 {t('tab_tools', 'en')}")
1897
+ with tab_tools:
1898
  with gr.Column(elem_classes=["whyx-tree"]):
1899
  # Tree root: Analyzer
1900
  with gr.Column(elem_classes=["whyx-tree-node"]):
 
2007
  )
2008
 
2009
  # ===== TAB 4: History =====
2010
+ tab_history = gr.TabItem(f"📜 {t('tab_history', 'en')}")
2011
+ with tab_history:
2012
  with gr.Column(elem_classes=["whyx-tree-node"]):
2013
  history_header_html = gr.HTML(_tree_header("📜", "history_section", "en"))
2014
  history_md = gr.HTML(
 
2032
  outputs=[],
2033
  )
2034
  web_search_clear.click(
2035
+ fn=lambda lang: f'<div class="whyx-info-text" style="color:var(--text-faint);font-size:12px;">{t("web_search_empty", "ru" if lang == "RU" else "en")}</div>',
2036
  inputs=[lang_state],
2037
  outputs=[web_search_output],
2038
  )
 
2189
  generate_btn.click(
2190
  fn=on_generate,
2191
  inputs=inputs_list,
2192
+ outputs=result_boxes + neg_boxes + result_cards,
2193
  ).then(
2194
  fn=lambda lang: gr.update(value=_format_history_html("ru" if lang == "RU" else "en")),
2195
  inputs=[lang_state],
 
2383
  updates.append(gr.update(value=_format_artist_info([], lc)))
2384
  updates.append(gr.update(value=f'<div class="whyx-info-text">{t("analyzer_desc", lc)}</div>'))
2385
  updates.append(gr.update(value=_format_history_html(lc)))
2386
+ updates.append(gr.update(label=f"✨ {t('tab_generate', lc)}"))
2387
+ updates.append(gr.update(label=f"🖼️ {t('tab_tagger', lc)}"))
2388
+ updates.append(gr.update(label=f"🔧 {t('tab_tools', lc)}"))
2389
+ updates.append(gr.update(label=f"📜 {t('tab_history', lc)}"))
2390
+ for _comp, kind in tooltip_spans:
2391
+ key = "tooltip_copy" if kind == "copy" else "tooltip_favorite"
2392
+ updates.append(gr.update(value=f'<span class="whyx-tooltip-text">{t(key, lc)}</span>'))
2393
  updates.append(choice)
2394
  return updates
2395
 
 
2427
  presets_acc, user_preset_name, user_preset_tags, user_preset_save, user_preset_delete,
2428
  user_preset_trigger, user_preset_dropdown, user_preset_apply, theme_toggle,
2429
  ] + category_checks + group_components + result_boxes + neg_boxes + [categories_desc_md, presets_desc_md, results_desc_md, hotkeys_hint_md, artist_info_md, analyzer_md, history_md,
2430
+ tab_generate, tab_tagger, tab_tools, tab_history] + [c for c, _k in tooltip_spans] + [lang_state]
2431
 
2432
  lang_btn.change(
2433
  fn=update_lang,
2434
  inputs=[lang_btn],
2435
  outputs=lang_outputs,
2436
  )
2437
+ lang_btn.change(
2438
+ fn=None,
2439
+ inputs=[lang_btn],
2440
+ outputs=[],
2441
+ js="(v) => { try { localStorage.setItem('whyx-lang', v); } catch(e) {} }",
2442
+ )
2443
 
2444
  theme_toggle.change(
2445
  fn=None,
2446
  inputs=[theme_toggle],
2447
  outputs=[],
2448
+ js="(v) => { document.body.classList.toggle('whyx-light', v === 'light'); try { localStorage.setItem('whyx-theme', v); } catch(e) {} }",
2449
+ )
2450
+
2451
+ # Restore persisted theme + language on page load: the JS reads
2452
+ # localStorage, applies the body class / radio value, then the standard
2453
+ # update_lang chain re-renders every localized label.
2454
+ demo.load(
2455
+ fn=None,
2456
+ inputs=[],
2457
+ outputs=[theme_toggle],
2458
+ js="() => { let t = 'dark'; try { t = localStorage.getItem('whyx-theme') || 'dark'; } catch(e) {} document.body.classList.toggle('whyx-light', t === 'light'); return t; }",
2459
+ ).then(
2460
+ fn=None,
2461
+ inputs=[],
2462
+ outputs=[lang_btn],
2463
+ js="() => { try { return localStorage.getItem('whyx-lang') || 'EN'; } catch(e) { return 'EN'; } }",
2464
+ ).then(
2465
+ fn=update_lang,
2466
+ inputs=[lang_btn],
2467
+ outputs=lang_outputs,
2468
  )
2469
 
2470
 
src/i18n.py CHANGED
@@ -361,6 +361,8 @@ L10N = {
361
  "tagger_tags_desc": "Edit the list, then Apply or Copy. Built from the toggled tags above.",
362
  "tagger_tags_ph": "Recognized tags will appear here…",
363
  "tagger_copy": "Copy",
 
 
364
  "tab_generate": "Generate",
365
  "tab_tagger": "Tagger",
366
  "tab_tools": "Tools",
@@ -750,6 +752,8 @@ L10N = {
750
  "tagger_tags_desc": "Отредактируйте список, затем «Применить» или «Копировать». Собран из отмеченных выше тегов.",
751
  "tagger_tags_ph": "Здесь появятся распознанные теги…",
752
  "tagger_copy": "Копировать",
 
 
753
  "tab_generate": "Генерация",
754
  "tab_tagger": "Теггер",
755
  "tab_tools": "Инструменты",
 
361
  "tagger_tags_desc": "Edit the list, then Apply or Copy. Built from the toggled tags above.",
362
  "tagger_tags_ph": "Recognized tags will appear here…",
363
  "tagger_copy": "Copy",
364
+ "tooltip_copy": "Copy to clipboard",
365
+ "tooltip_favorite": "Toggle favorite",
366
  "tab_generate": "Generate",
367
  "tab_tagger": "Tagger",
368
  "tab_tools": "Tools",
 
752
  "tagger_tags_desc": "Отредактируйте список, затем «Применить» или «Копировать». Собран из отмеченных выше тегов.",
753
  "tagger_tags_ph": "Здесь появятся распознанные теги…",
754
  "tagger_copy": "Копировать",
755
+ "tooltip_copy": "Скопировать в буфер",
756
+ "tooltip_favorite": "Добавить в избранное",
757
  "tab_generate": "Генерация",
758
  "tab_tagger": "Теггер",
759
  "tab_tools": "Инструменты",
src/prompt_analyzer.py CHANGED
@@ -241,7 +241,7 @@ def format_analysis_html(data: dict, lang: str) -> str:
241
  if data.get("error") == "parse":
242
  msg = t("analyzer_parse_error", lang)
243
  return f"""
244
- <div style="background:rgba(15,23,42,0.5);border:1px solid rgba(239,68,68,0.25);border-radius:10px;padding:10px 14px;margin-top:4px;">
245
  <div style="color:#F87171;font-size:12px;">⚠ {msg}</div>
246
  </div>
247
  """
@@ -266,22 +266,22 @@ def format_analysis_html(data: dict, lang: str) -> str:
266
 
267
  lines = []
268
 
269
- lines.append("<div style='background:rgba(15,23,42,0.5);border:1px solid rgba(56,189,248,0.12);border-radius:10px;padding:10px 14px;margin-top:4px;'>")
270
 
271
- lines.append(f"<div style='display:flex;gap:12px;flex-wrap:wrap;font-size:11px;color:#94A3B8;margin-bottom:6px;'>")
272
- lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_subject', lang)}:</strong> {html.escape(s['subject'])}</span>")
273
- lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_character', lang)}:</strong> {html.escape(s['character'])}</span>")
274
- lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_series', lang)}:</strong> {html.escape(s['series'])}</span>")
275
  if s['artists']:
276
- lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_artists', lang)}:</strong> {html.escape(', '.join(s['artists']))}</span>")
277
  lines.append("</div>")
278
 
279
  bar_color = {"good": "#34D399", "missing": "#F87171", "conflict": "#FBBF24", "low": "#FBBF24"}.get(q["status"], "#94A3B8")
280
 
281
- lines.append(f"<div style='display:flex;gap:10px;flex-wrap:wrap;font-size:11px;color:#94A3B8;margin-bottom:6px;'>")
282
- lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_tags', lang)}:</strong> {s['total_tags']} ({t('analyzer_quality', lang)}: {s['num_quality']}, {t('analyzer_meta', lang)}: {s['num_meta']}, {t('analyzer_general', lang)}: {s['num_general']})</span>")
283
- lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_quality', lang)}:</strong> <span style='color:{bar_color};'>{quality_label}</span></span>")
284
- lines.append(f"<span><strong style='color:#E2E8F0;'>{t('analyzer_tokens', lang)}:</strong> ≈{tok['clip']} CLIP {'⚠️' if tok['clip_warning'] else ''} / ≈{tok['t5']} T5 {'⚠️' if tok['t5_warning'] else ''}</span>")
285
  if s['n_language']:
286
  lines.append(f"<span style='color:#818CF8;'>{t('analyzer_natural_lang', lang)}</span>")
287
  lines.append("</div>")
 
241
  if data.get("error") == "parse":
242
  msg = t("analyzer_parse_error", lang)
243
  return f"""
244
+ <div style="background:var(--input-bg);border:1px solid rgba(239,68,68,0.25);border-radius:10px;padding:10px 14px;margin-top:4px;">
245
  <div style="color:#F87171;font-size:12px;">⚠ {msg}</div>
246
  </div>
247
  """
 
266
 
267
  lines = []
268
 
269
+ lines.append("<div style='background:var(--input-bg);border:1px solid rgba(56,189,248,0.12);border-radius:10px;padding:10px 14px;margin-top:4px;'>")
270
 
271
+ lines.append(f"<div style='display:flex;gap:12px;flex-wrap:wrap;font-size:11px;color:var(--text-dim);margin-bottom:6px;'>")
272
+ lines.append(f"<span><strong style='color:var(--text);'>{t('analyzer_subject', lang)}:</strong> {html.escape(s['subject'])}</span>")
273
+ lines.append(f"<span><strong style='color:var(--text);'>{t('analyzer_character', lang)}:</strong> {html.escape(s['character'])}</span>")
274
+ lines.append(f"<span><strong style='color:var(--text);'>{t('analyzer_series', lang)}:</strong> {html.escape(s['series'])}</span>")
275
  if s['artists']:
276
+ lines.append(f"<span><strong style='color:var(--text);'>{t('analyzer_artists', lang)}:</strong> {html.escape(', '.join(s['artists']))}</span>")
277
  lines.append("</div>")
278
 
279
  bar_color = {"good": "#34D399", "missing": "#F87171", "conflict": "#FBBF24", "low": "#FBBF24"}.get(q["status"], "#94A3B8")
280
 
281
+ lines.append(f"<div style='display:flex;gap:10px;flex-wrap:wrap;font-size:11px;color:var(--text-dim);margin-bottom:6px;'>")
282
+ lines.append(f"<span><strong style='color:var(--text);'>{t('analyzer_tags', lang)}:</strong> {s['total_tags']} ({t('analyzer_quality', lang)}: {s['num_quality']}, {t('analyzer_meta', lang)}: {s['num_meta']}, {t('analyzer_general', lang)}: {s['num_general']})</span>")
283
+ lines.append(f"<span><strong style='color:var(--text);'>{t('analyzer_quality', lang)}:</strong> <span style='color:{bar_color};'>{quality_label}</span></span>")
284
+ lines.append(f"<span><strong style='color:var(--text);'>{t('analyzer_tokens', lang)}:</strong> ≈{tok['clip']} CLIP {'⚠️' if tok['clip_warning'] else ''} / ≈{tok['t5']} T5 {'⚠️' if tok['t5_warning'] else ''}</span>")
285
  if s['n_language']:
286
  lines.append(f"<span style='color:#818CF8;'>{t('analyzer_natural_lang', lang)}</span>")
287
  lines.append("</div>")