aidn commited on
Commit
d8f50d3
Β·
verified Β·
1 Parent(s): 0115760

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +189 -136
app.py CHANGED
@@ -1,4 +1,5 @@
1
  import os
 
2
  import gradio as gr
3
  from huggingface_hub import InferenceClient
4
 
@@ -8,74 +9,155 @@ MODEL_ID = "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8"
8
 
9
  # ── System Prompts ─────────────────────────────────────────────────────────────
10
 
11
- PROMPT_TO_LINKEDIN = """Du bist ein LinkedIn-Influencer-Generator. Deine einzige Aufgabe ist es, banale, alltΓ€gliche Aussagen in absurd ausschweifende, klischeebeladene LinkedIn-Posts zu verwandeln.
12
 
13
  Regeln:
14
  - Alles ist eine "Journey", ein "Gamechanger" oder eine "powerful lesson"
15
  - Nutze mindestens 3 Emojis strategisch
16
- - ErwΓ€hne "Growth", "Mindset", "Passion" oder "Impact" wo immer mΓΆglich
17
- - FΓΌge eine persΓΆnliche Anekdote hinzu, die niemand braucht
18
  - Endet mit einer rhetorischen Frage an die Community
19
- - Benutze dramatische ZeilenumbrΓΌche fΓΌr Effekt
20
  - Alles ist ausnahmslos positiv, auch wenn das Original negativ ist
21
  - Hashtags am Ende sind Pflicht (mindestens 5)
22
- - Klingt wie jemand, der gerade ein Buch ΓΌber sich selbst schreiben wΓΌrde
23
- - Formatiere mit Markdown: Erâffnungssatz als ## Überschrift, Schlüsselbegriffe wie **Gamechanger**, **Growth**, **Mindset**, **Journey** fett hervorheben, Abschnitte mit Leerzeilen trennen
24
 
25
- Antworte NUR mit dem LinkedIn-Post in Markdown. Kein Vorwort, keine ErklΓ€rung."""
26
 
27
  PROMPT_FROM_LINKEDIN = """Du bist ein gnadenloser semantischer Reduzierer. Du hasst Floskeln. Deine Aufgabe: LinkedIn-Texte auf das absolute, brutalste Minimum eindampfen.
28
 
29
  Regeln:
30
  - EIN Satz. Nicht zwei. Einer.
31
- - KΓΌrze bis es wehtut. Dann nochmal kΓΌrzen.
32
  - Null Emotion, null Wertung, null Kontext der niemanden interessiert
33
- - Streiche alles was keine neue Information trΓ€gt: kein "Ich bin dankbar", kein "Diese Erfahrung hat mich gelehrt", kein "Lasst mich das erklΓ€ren"
34
- - Wenn der gesamte Post nur bedeutet "Ich hab heute Kaffee getrunken" – schreib genau das
35
- - Maximal 15 WΓΆrter
36
 
37
- Antworte NUR mit diesem einen Satz. Kein Punkt dahinter wenn er zu selbstgefΓ€llig wirkt."""
38
 
 
39
 
40
- # ── LLM-Call ──────────────────────────────────────────────────────────────────
41
 
42
- def translate(text: str, direction: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  if not text.strip():
44
  return ""
45
  if not HF_TOKEN:
46
- return "⚠️ Kein HF_TOKEN gefunden. Bitte in den Space-Settings als Secret hinterlegen."
47
-
48
- system_prompt = PROMPT_TO_LINKEDIN if direction == "to_linkedin" else PROMPT_FROM_LINKEDIN
49
-
50
  try:
51
- client = InferenceClient(provider="novita", api_key=HF_TOKEN)
52
- response = client.chat.completions.create(
53
- model=MODEL_ID,
54
- messages=[
55
- {"role": "system", "content": system_prompt},
56
- {"role": "user", "content": text},
57
- ],
58
- max_tokens=1024,
59
- )
60
- return response.choices[0].message.content.strip()
61
  except Exception as e:
62
- return f"⚠️ Fehler: {e}"
63
-
64
 
65
- # ── State & Handler ────────────────────────────────────────────────────────────
66
 
67
- # direction: "to_linkedin" β†’ Normal β†’ LinkedIn
68
- # "from_linkedin" β†’ LinkedIn β†’ Normal
69
-
70
- def swap_direction(current_dir, input_text, output_text):
71
- """Tauscht Richtung und Felder."""
72
- new_dir = "from_linkedin" if current_dir == "to_linkedin" else "to_linkedin"
73
- return (
74
- new_dir,
75
- output_text, # altes Output wird neuer Input
76
- input_text, # alter Input wird neues Output
77
- *_labels(new_dir),
78
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
  def _labels(direction):
81
  if direction == "to_linkedin":
@@ -83,9 +165,21 @@ def _labels(direction):
83
  else:
84
  return "πŸ’Ό LinkedIn Speech", "✍️ Normale Aussage", "πŸ”„ β†’ Entbuzzen"
85
 
 
 
 
 
 
 
 
 
86
  def run_translate(text, direction):
87
  result = translate(text, direction)
88
- return result, result
 
 
 
 
89
 
90
 
91
  # ── CSS ───────────────────────────────────────────────────────────────────────
@@ -102,12 +196,10 @@ CSS = """
102
  --li-muted: #666666;
103
  --li-border: #E0DFDC;
104
  }
105
-
106
  body, .gradio-container {
107
  background: var(--li-bg) !important;
108
  font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important;
109
  }
110
-
111
  .li-header {
112
  background: linear-gradient(135deg, var(--li-blue-dark) 0%, var(--li-blue) 70%, var(--li-blue-mid) 100%);
113
  border-radius: 12px;
@@ -122,89 +214,41 @@ body, .gradio-container {
122
  .li-header h1 { margin: 0 !important; font-size: 1.65rem !important; font-weight: 700 !important; color: #fff !important; }
123
  .li-header p { margin: 4px 0 0 !important; font-size: .86rem !important; color: rgba(255,255,255,.88) !important; }
124
  .li-header .badge {
125
- margin-left: auto;
126
- background: rgba(255,255,255,.18);
127
- border-radius: 20px;
128
- padding: 5px 14px;
129
- font-size: .74rem;
130
- font-weight: 600;
131
- letter-spacing: .4px;
132
- color: #fff;
133
- white-space: nowrap;
134
  }
135
-
136
  .direction-banner {
137
- text-align: center;
138
- font-size: .8rem;
139
- font-weight: 700;
140
- letter-spacing: .6px;
141
- text-transform: uppercase;
142
- color: var(--li-blue-dark);
143
- margin-bottom: 6px;
144
  }
145
-
146
  label > span {
147
- font-weight: 600 !important;
148
- font-size: .76rem !important;
149
- text-transform: uppercase !important;
150
- letter-spacing: .5px !important;
151
- color: var(--li-blue-dark) !important;
152
  }
153
-
154
  textarea {
155
- font-size: .9rem !important;
156
- line-height: 1.7 !important;
157
- border-color: var(--li-border) !important;
158
- border-radius: 8px !important;
159
- background: var(--li-card) !important;
160
- color: var(--li-text) !important;
161
- resize: vertical !important;
162
  }
163
  textarea:focus {
164
  border-color: var(--li-blue) !important;
165
- box-shadow: 0 0 0 2px var(--li-blue-light) !important;
166
- outline: none !important;
167
  }
168
-
169
  button.primary {
170
- background: var(--li-blue) !important;
171
- border-radius: 22px !important;
172
- border: none !important;
173
- font-weight: 700 !important;
174
- font-size: 1rem !important;
175
- padding: 10px 32px !important;
176
- box-shadow: 0 2px 10px rgba(10,102,194,.35) !important;
177
- transition: background .15s, box-shadow .15s !important;
178
- }
179
- button.primary:hover {
180
- background: var(--li-blue-dark) !important;
181
- box-shadow: 0 4px 18px rgba(10,102,194,.45) !important;
182
  }
183
-
184
  button.secondary {
185
- background: var(--li-card) !important;
186
- color: var(--li-blue) !important;
187
- border: 2px solid var(--li-blue) !important;
188
- border-radius: 22px !important;
189
- font-weight: 700 !important;
190
- font-size: 1rem !important;
191
- padding: 10px 28px !important;
192
- transition: all .15s !important;
193
- }
194
- button.secondary:hover {
195
- background: var(--li-blue-light) !important;
196
  }
197
-
198
  .li-footer {
199
- font-size: .74rem;
200
- color: var(--li-muted);
201
- border-top: 1px solid var(--li-border);
202
- padding-top: 10px;
203
- margin-top: 8px;
204
- display: flex;
205
- gap: 20px;
206
- flex-wrap: wrap;
207
- justify-content: center;
208
  }
209
  """
210
 
@@ -218,14 +262,14 @@ with gr.Blocks(title="LinkedIn Translator", css=CSS) as demo:
218
  <div class="li-header">
219
  <div>
220
  <h1>LinkedIn Translator</h1>
221
- <p>Banale Wahrheit ↔ Epische LinkedIn-Prosa &nbsp;Β·&nbsp; powered by Llama 4</p>
222
  </div>
223
- <div class="badge">✨ AI-Powered</div>
224
  </div>
225
  """)
226
 
227
  dir_label = gr.HTML(
228
- '<div class="direction-banner">Modus: Normale Sprache β†’ LinkedIn Speech</div>'
229
  )
230
 
231
  with gr.Row(equal_height=True):
@@ -237,10 +281,9 @@ with gr.Blocks(title="LinkedIn Translator", css=CSS) as demo:
237
  )
238
  with gr.Column(scale=1, min_width=120):
239
  gr.HTML("<div style='height:40px'></div>")
240
- translate_btn = gr.Button("β–Ά Übersetzen", variant="primary", size="lg")
241
  gr.HTML("<div style='height:12px'></div>")
242
- swap_btn = gr.Button("πŸ”„ β†’ LinkedIn", variant="secondary", size="sm",
243
- elem_id="swap_btn")
244
  with gr.Column(scale=5):
245
  output_box = gr.Textbox(
246
  label="πŸ’Ό LinkedIn Speech",
@@ -249,21 +292,20 @@ with gr.Blocks(title="LinkedIn Translator", css=CSS) as demo:
249
  interactive=False,
250
  )
251
 
252
- # ── Markdown-Vorschau ──────────────────────────────────────────────────────
253
  with gr.Row():
254
  with gr.Column():
255
- gr.HTML('<div style="font-weight:700;font-size:.76rem;text-transform:uppercase;letter-spacing:.5px;color:#004182;margin-bottom:4px;">πŸ“‹&nbsp; Gerenderte Vorschau</div>')
256
  markdown_out = gr.Markdown(
257
  value="*Noch kein Ergebnis – bitte zuerst ΓΌbersetzen.*",
258
- elem_id="md_preview",
259
  )
 
260
 
261
  if not HF_TOKEN:
262
  gr.HTML("""
263
  <div style="background:#FFF4CE;border:1px solid #F9C642;border-left:4px solid #F9C642;
264
  border-radius:6px;padding:10px 14px;font-size:.85rem;color:#7A5800;margin-top:8px;">
265
- <strong>⚠️ Kein HF_TOKEN gefunden.</strong>
266
- FΓΌge ihn unter <em>Settings β†’ Variables and secrets</em> als <code>HF_TOKEN</code> hinzu.
267
  </div>
268
  """)
269
 
@@ -271,33 +313,44 @@ with gr.Blocks(title="LinkedIn Translator", css=CSS) as demo:
271
  <div class="li-footer">
272
  <span>🧠 Llama 4 Maverick 17B</span>
273
  <span>πŸ”„ Bidirektional</span>
274
- <span>πŸ’‘ Ironie inklusive</span>
275
  </div>
276
  """)
277
 
278
- # ── Event-Handler ──────────────────────────────────────────────────────────
279
 
280
  translate_btn.click(
281
  fn=run_translate,
282
  inputs=[input_box, direction_state],
283
- outputs=[output_box, markdown_out],
284
  )
285
 
286
  def do_swap(direction, inp, out):
287
  new_dir, new_inp, new_out, lbl_in, lbl_out, btn_txt = swap_direction(direction, inp, out)
288
- banner = f'<div class="direction-banner">Modus: {lbl_in.split(" ", 1)[1]} β†’ {lbl_out.split(" ", 1)[1]}</div>'
289
- return (
290
- new_dir,
291
- gr.update(value=new_inp, label=lbl_in),
292
- gr.update(value=new_out, label=lbl_out),
293
- gr.update(value=btn_txt),
294
- banner,
295
  )
 
 
 
 
 
 
 
 
 
 
 
296
 
297
  swap_btn.click(
298
  fn=do_swap,
299
  inputs=[direction_state, input_box, output_box],
300
- outputs=[direction_state, input_box, output_box, swap_btn, dir_label],
 
301
  )
302
 
303
  demo.launch()
 
1
  import os
2
+ import json
3
  import gradio as gr
4
  from huggingface_hub import InferenceClient
5
 
 
9
 
10
  # ── System Prompts ─────────────────────────────────────────────────────────────
11
 
12
+ PROMPT_TO_LINKEDIN = """Du bist ein LinkedIn-Influencer-Generator. Deine einzige Aufgabe ist es, banale, alltaegliche Aussagen in absurd ausschweifende, klischeebeladene LinkedIn-Posts zu verwandeln.
13
 
14
  Regeln:
15
  - Alles ist eine "Journey", ein "Gamechanger" oder eine "powerful lesson"
16
  - Nutze mindestens 3 Emojis strategisch
17
+ - Erwaehne "Growth", "Mindset", "Passion" oder "Impact" wo immer moeglich
18
+ - Fuege eine persoenliche Anekdote hinzu, die niemand braucht
19
  - Endet mit einer rhetorischen Frage an die Community
20
+ - Benutze dramatische Zeilenumbrueche fuer Effekt
21
  - Alles ist ausnahmslos positiv, auch wenn das Original negativ ist
22
  - Hashtags am Ende sind Pflicht (mindestens 5)
23
+ - Klingt wie jemand, der gerade ein Buch ueber sich selbst schreiben wuerde
24
+ - Formatiere mit Markdown: Eroeffnungssatz als ## Ueberschrift, Schluesselbegriffe wie **Gamechanger**, **Growth**, **Mindset**, **Journey** fett hervorheben, Abschnitte mit Leerzeilen trennen
25
 
26
+ Antworte NUR mit dem LinkedIn-Post in Markdown. Kein Vorwort, keine Erklaerung."""
27
 
28
  PROMPT_FROM_LINKEDIN = """Du bist ein gnadenloser semantischer Reduzierer. Du hasst Floskeln. Deine Aufgabe: LinkedIn-Texte auf das absolute, brutalste Minimum eindampfen.
29
 
30
  Regeln:
31
  - EIN Satz. Nicht zwei. Einer.
32
+ - Kuerze bis es wehtut. Dann nochmal kuerzen.
33
  - Null Emotion, null Wertung, null Kontext der niemanden interessiert
34
+ - Streiche alles was keine neue Information traegt
35
+ - Wenn der gesamte Post nur bedeutet "Ich hab heute Kaffee getrunken" schreib genau das
36
+ - Maximal 15 Woerter
37
 
38
+ Antworte NUR mit diesem einen Satz. Kein Vorwort, keine Erklaerung."""
39
 
40
+ PROMPT_BINGO = """Du bist ein sarkastischer LinkedIn-Prosa-Analytiker. Bewerte den folgenden LinkedIn-Post anhand von 5 Metriken.
41
 
42
+ Antworte AUSSCHLIESSLICH mit einem validen JSON-Objekt. Kein Markdown, keine Backticks, keine ErklΓ€rung davor oder danach. Nur das rohe JSON.
43
 
44
+ Format:
45
+ {"metrics":[{"icon":"X","label":"Y","score":N,"comment":"Z"},...], "verdict":"..."}
46
+
47
+ Verwende diese 5 Metriken in dieser Reihenfolge:
48
+ 1. icon="Buzzword-Dichte" label="Buzzword-Dichte" - Wie viele inhaltsleere Modebegriffe?
49
+ 2. icon="Laenge vs. Inhalt" label="Laenge vs. Inhalt" - Wie viel Laenge fuer wie wenig Aussage?
50
+ 3. icon="Selbstbeweihraeuche" label="Selbstbeweihraeuche" - Wie sehr dreht sich alles um den Autor?
51
+ 4. icon="Hashtag-Overload" label="Hashtag-Overload" - Hashtag-Dichte und Sinnlosigkeit
52
+ 5. icon="Sinnlosigkeits-Index" label="Sinnlosigkeits-Index" - Koennte man den Post loeschen ohne Informationsverlust?
53
+
54
+ score ist ein Integer von 1-10. 10 = maximaler LinkedIn-Exzess.
55
+ comment ist max. 6 Woerter, sarkastisch.
56
+ verdict ist ein einziger vernichtender Satz, max. 12 Woerter."""
57
+
58
+
59
+ # ── LLM-Calls ─────────────────────────────────────────────────────────────────
60
+
61
+ def _call_llm(system, user, max_tokens=1024):
62
+ client = InferenceClient(provider="novita", api_key=HF_TOKEN)
63
+ resp = client.chat.completions.create(
64
+ model=MODEL_ID,
65
+ messages=[{"role": "system", "content": system},
66
+ {"role": "user", "content": user}],
67
+ max_tokens=max_tokens,
68
+ )
69
+ return resp.choices[0].message.content.strip()
70
+
71
+
72
+ def translate(text, direction):
73
  if not text.strip():
74
  return ""
75
  if not HF_TOKEN:
76
+ return "Kein HF_TOKEN gefunden."
77
+ prompt = PROMPT_TO_LINKEDIN if direction == "to_linkedin" else PROMPT_FROM_LINKEDIN
 
 
78
  try:
79
+ return _call_llm(prompt, text)
 
 
 
 
 
 
 
 
 
80
  except Exception as e:
81
+ return f"Fehler: {e}"
 
82
 
 
83
 
84
+ def get_bingo(text):
85
+ if not text.strip() or not HF_TOKEN:
86
+ return ""
87
+ try:
88
+ raw = _call_llm(PROMPT_BINGO, text, max_tokens=512)
89
+ start = raw.find("{")
90
+ end = raw.rfind("}") + 1
91
+ data = json.loads(raw[start:end])
92
+ return _render_bingo(data)
93
+ except Exception as e:
94
+ return f"<p style='color:#c00'>Analyse fehlgeschlagen: {e}</p>"
95
+
96
+
97
+ def _render_bingo(data):
98
+ metrics = data.get("metrics", [])
99
+ verdict = data.get("verdict", "")
100
+
101
+ ICONS = {
102
+ "Buzzword-Dichte": "πŸ—£οΈ",
103
+ "LΓ€nge vs. Inhalt": "πŸ“",
104
+ "SelbstbeweihrΓ€uche": "πŸͺž",
105
+ "Hashtag-Overload": "#️⃣",
106
+ "Sinnlosigkeits-Index": "πŸŒ€",
107
+ }
108
+
109
+ def bar_color(s):
110
+ if s >= 8: return "#C0392B"
111
+ if s >= 5: return "#E67E22"
112
+ return "#27AE60"
113
+
114
+ rows = ""
115
+ for m in metrics:
116
+ score = int(m.get("score", 0))
117
+ label = m.get("label", m.get("icon", ""))
118
+ icon = ICONS.get(label, m.get("icon", ""))
119
+ color = bar_color(score)
120
+ pct = score * 10
121
+ rows += f"""
122
+ <div style="margin-bottom:14px;">
123
+ <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:5px;">
124
+ <span style="font-size:.88rem;font-weight:600;color:#191919;">{icon} {label}</span>
125
+ <span style="font-size:.78rem;color:#888;font-style:italic;margin:0 8px;">{m.get('comment','')}</span>
126
+ <span style="font-size:1.05rem;font-weight:700;color:{color};min-width:24px;text-align:right;">{score}</span>
127
+ </div>
128
+ <div style="background:#E0DFDC;border-radius:99px;height:9px;overflow:hidden;">
129
+ <div style="width:{pct}%;background:{color};height:100%;border-radius:99px;"></div>
130
+ </div>
131
+ </div>"""
132
+
133
+ total = sum(int(m.get("score", 0)) for m in metrics)
134
+ max_score = len(metrics) * 10
135
+ total_pct = round(total / max_score * 100) if max_score else 0
136
+
137
+ badge_color = "#C0392B" if total_pct >= 70 else "#E67E22" if total_pct >= 40 else "#27AE60"
138
+
139
+ return f"""
140
+ <div style="background:#fff;border:1px solid #E0DFDC;border-radius:12px;
141
+ padding:22px 26px;margin-top:4px;
142
+ box-shadow:0 2px 12px rgba(0,0,0,.07);">
143
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:20px;">
144
+ <span style="font-size:1.3rem;">🎯</span>
145
+ <span style="font-weight:700;font-size:.95rem;color:#004182;
146
+ text-transform:uppercase;letter-spacing:.5px;">Corporate Nonsense Score</span>
147
+ <span style="margin-left:auto;background:{badge_color};color:#fff;
148
+ border-radius:99px;padding:4px 14px;font-size:.85rem;font-weight:700;">
149
+ {total}&thinsp;/&thinsp;{max_score} &nbsp;&middot;&nbsp; {total_pct}%
150
+ </span>
151
+ </div>
152
+ {rows}
153
+ <div style="margin-top:18px;padding-top:14px;border-top:1px solid #E0DFDC;
154
+ font-size:.88rem;color:#444;font-style:italic;line-height:1.6;">
155
+ πŸ’¬ <strong style="font-style:normal;">Urteil:</strong> {verdict}
156
+ </div>
157
+ </div>"""
158
+
159
+
160
+ # ── Labels & Swap ────────────────────────────────────────────────────────��─────
161
 
162
  def _labels(direction):
163
  if direction == "to_linkedin":
 
165
  else:
166
  return "πŸ’Ό LinkedIn Speech", "✍️ Normale Aussage", "πŸ”„ β†’ Entbuzzen"
167
 
168
+
169
+ def swap_direction(current_dir, inp, out):
170
+ new_dir = "from_linkedin" if current_dir == "to_linkedin" else "to_linkedin"
171
+ return new_dir, out, inp, *_labels(new_dir)
172
+
173
+
174
+ # ── Haupt-Handler ──────────────────────────────────────────────────────────────
175
+
176
  def run_translate(text, direction):
177
  result = translate(text, direction)
178
+ if direction == "to_linkedin":
179
+ return result, gr.update(value=result, visible=True), gr.update(value="", visible=False)
180
+ else:
181
+ bingo_html = get_bingo(text)
182
+ return result, gr.update(value="", visible=False), gr.update(value=bingo_html, visible=True)
183
 
184
 
185
  # ── CSS ───────────────────────────────────────────────────────────────────────
 
196
  --li-muted: #666666;
197
  --li-border: #E0DFDC;
198
  }
 
199
  body, .gradio-container {
200
  background: var(--li-bg) !important;
201
  font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important;
202
  }
 
203
  .li-header {
204
  background: linear-gradient(135deg, var(--li-blue-dark) 0%, var(--li-blue) 70%, var(--li-blue-mid) 100%);
205
  border-radius: 12px;
 
214
  .li-header h1 { margin: 0 !important; font-size: 1.65rem !important; font-weight: 700 !important; color: #fff !important; }
215
  .li-header p { margin: 4px 0 0 !important; font-size: .86rem !important; color: rgba(255,255,255,.88) !important; }
216
  .li-header .badge {
217
+ margin-left: auto; background: rgba(255,255,255,.18); border-radius: 20px;
218
+ padding: 5px 14px; font-size: .74rem; font-weight: 600; letter-spacing: .4px; color: #fff; white-space: nowrap;
 
 
 
 
 
 
 
219
  }
 
220
  .direction-banner {
221
+ text-align: center; font-size: .8rem; font-weight: 700; letter-spacing: .6px;
222
+ text-transform: uppercase; color: var(--li-blue-dark); margin-bottom: 6px;
 
 
 
 
 
223
  }
 
224
  label > span {
225
+ font-weight: 600 !important; font-size: .76rem !important; text-transform: uppercase !important;
226
+ letter-spacing: .5px !important; color: var(--li-blue-dark) !important;
 
 
 
227
  }
 
228
  textarea {
229
+ font-size: .9rem !important; line-height: 1.7 !important; border-color: var(--li-border) !important;
230
+ border-radius: 8px !important; background: var(--li-card) !important;
231
+ color: var(--li-text) !important; resize: vertical !important;
 
 
 
 
232
  }
233
  textarea:focus {
234
  border-color: var(--li-blue) !important;
235
+ box-shadow: 0 0 0 2px var(--li-blue-light) !important; outline: none !important;
 
236
  }
 
237
  button.primary {
238
+ background: var(--li-blue) !important; border-radius: 22px !important; border: none !important;
239
+ font-weight: 700 !important; font-size: 1rem !important; padding: 10px 32px !important;
240
+ box-shadow: 0 2px 10px rgba(10,102,194,.35) !important; transition: background .15s, box-shadow .15s !important;
 
 
 
 
 
 
 
 
 
241
  }
242
+ button.primary:hover { background: var(--li-blue-dark) !important; box-shadow: 0 4px 18px rgba(10,102,194,.45) !important; }
243
  button.secondary {
244
+ background: var(--li-card) !important; color: var(--li-blue) !important;
245
+ border: 2px solid var(--li-blue) !important; border-radius: 22px !important;
246
+ font-weight: 700 !important; font-size: 1rem !important; padding: 10px 28px !important; transition: all .15s !important;
 
 
 
 
 
 
 
 
247
  }
248
+ button.secondary:hover { background: var(--li-blue-light) !important; }
249
  .li-footer {
250
+ font-size: .74rem; color: var(--li-muted); border-top: 1px solid var(--li-border);
251
+ padding-top: 10px; margin-top: 8px; display: flex; gap: 20px; flex-wrap: wrap; justify-content: center;
 
 
 
 
 
 
 
252
  }
253
  """
254
 
 
262
  <div class="li-header">
263
  <div>
264
  <h1>LinkedIn Translator</h1>
265
+ <p>Banale Wahrheit &harr; Epische LinkedIn-Prosa &nbsp;&middot;&nbsp; powered by Llama 4</p>
266
  </div>
267
+ <div class="badge">&#10024; AI-Powered</div>
268
  </div>
269
  """)
270
 
271
  dir_label = gr.HTML(
272
+ '<div class="direction-banner">Modus: Normale Sprache &rarr; LinkedIn Speech</div>'
273
  )
274
 
275
  with gr.Row(equal_height=True):
 
281
  )
282
  with gr.Column(scale=1, min_width=120):
283
  gr.HTML("<div style='height:40px'></div>")
284
+ translate_btn = gr.Button("Übersetzen", variant="primary", size="lg")
285
  gr.HTML("<div style='height:12px'></div>")
286
+ swap_btn = gr.Button("πŸ”„ β†’ LinkedIn", variant="secondary", size="sm")
 
287
  with gr.Column(scale=5):
288
  output_box = gr.Textbox(
289
  label="πŸ’Ό LinkedIn Speech",
 
292
  interactive=False,
293
  )
294
 
 
295
  with gr.Row():
296
  with gr.Column():
 
297
  markdown_out = gr.Markdown(
298
  value="*Noch kein Ergebnis – bitte zuerst ΓΌbersetzen.*",
299
+ visible=True,
300
  )
301
+ bingo_out = gr.HTML(value="", visible=False)
302
 
303
  if not HF_TOKEN:
304
  gr.HTML("""
305
  <div style="background:#FFF4CE;border:1px solid #F9C642;border-left:4px solid #F9C642;
306
  border-radius:6px;padding:10px 14px;font-size:.85rem;color:#7A5800;margin-top:8px;">
307
+ <strong>Kein HF_TOKEN gefunden.</strong>
308
+ Unter <em>Settings &rarr; Variables and secrets</em> als <code>HF_TOKEN</code> hinzufuegen.
309
  </div>
310
  """)
311
 
 
313
  <div class="li-footer">
314
  <span>🧠 Llama 4 Maverick 17B</span>
315
  <span>πŸ”„ Bidirektional</span>
316
+ <span>🎯 Corporate Nonsense Score</span>
317
  </div>
318
  """)
319
 
320
+ # ── Events ────────────────────────────────────────────────────────────────
321
 
322
  translate_btn.click(
323
  fn=run_translate,
324
  inputs=[input_box, direction_state],
325
+ outputs=[output_box, markdown_out, bingo_out],
326
  )
327
 
328
  def do_swap(direction, inp, out):
329
  new_dir, new_inp, new_out, lbl_in, lbl_out, btn_txt = swap_direction(direction, inp, out)
330
+ banner = (
331
+ '<div class="direction-banner">Modus: '
332
+ + lbl_in.split(" ", 1)[1]
333
+ + " &rarr; "
334
+ + lbl_out.split(" ", 1)[1]
335
+ + "</div>"
 
336
  )
337
+ if new_dir == "to_linkedin":
338
+ md_upd = gr.update(value="*Noch kein Ergebnis.*", visible=True)
339
+ bingo_upd = gr.update(value="", visible=False)
340
+ else:
341
+ md_upd = gr.update(value="", visible=False)
342
+ bingo_upd = gr.update(value="", visible=True)
343
+ return (new_dir,
344
+ gr.update(value=new_inp, label=lbl_in),
345
+ gr.update(value=new_out, label=lbl_out),
346
+ gr.update(value=btn_txt),
347
+ banner, md_upd, bingo_upd)
348
 
349
  swap_btn.click(
350
  fn=do_swap,
351
  inputs=[direction_state, input_box, output_box],
352
+ outputs=[direction_state, input_box, output_box, swap_btn, dir_label,
353
+ markdown_out, bingo_out],
354
  )
355
 
356
  demo.launch()