k96beni commited on
Commit
9e3f44e
·
verified ·
1 Parent(s): efee163

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +213 -394
app.py CHANGED
@@ -1,394 +1,213 @@
1
- import os
2
- import pandas as pd
3
- import torch
4
- import gradio as gr
5
- import textwrap # för chunk-exempel
6
-
7
- from huggingface_hub import login, Repository
8
- from sentence_transformers import SentenceTransformer, util
9
-
10
- # ====================================================
11
- # KONFIGURATION OCH INIT
12
- # ====================================================
13
- HF_TOKEN = os.getenv("HUGGINGFACE_TOKEN") # Miljövariabel med din Hugging Face-token
14
- if not HF_TOKEN:
15
- raise ValueError("HUGGINGFACE_TOKEN miljövariabel saknas!")
16
-
17
- login(token=HF_TOKEN)
18
-
19
- # Ange var du har din Space och CSV
20
- REPO_ID = "ChargeNodeEurope/Chatbot_4o_mini" # Exempel
21
- REPO_LOCAL_PATH = "chatbot_faq_repo"
22
- FAQ_CSV_PATH = "faq_städad_utf8_fixed.csv"
23
-
24
- # Klona från Space:en (OBS: repo_type="space")
25
- repo = Repository(
26
- local_dir=REPO_LOCAL_PATH,
27
- clone_from=REPO_ID,
28
- repo_type="space",
29
- use_auth_token=HF_TOKEN
30
- )
31
-
32
- # Ladda FAQ
33
- faq_path = os.path.join(REPO_LOCAL_PATH, FAQ_CSV_PATH)
34
- try:
35
- df = pd.read_csv(faq_path, sep=";")
36
- df.dropna(subset=["Fråga", "Svar"], inplace=True)
37
- except Exception as e:
38
- raise FileNotFoundError(f"Kunde inte ladda FAQ-filen: {str(e)}")
39
-
40
- # -- Standardparametrar vi kan ändra i Admin-UI --
41
- CHUNK_SIZE = 0 # 0 innebär ingen chunkning som default
42
- TEMPERATURE = 0.0 # "Heat param", 0 = ingen slump, 1 = maximal slump (demonstration)
43
-
44
- # Ladda modell
45
- model_name = "sentence-transformers/all-MiniLM-L6-v2"
46
- model = SentenceTransformer(model_name)
47
-
48
- # ------------------------------------------------
49
- # ENKEL CHUNKING (illustrativt exempel)
50
- # ------------------------------------------------
51
- def chunk_text(text: str, chunk_size: int):
52
- """
53
- Delar upp en textsträng i bitar om 'chunk_size' tecken.
54
- Returnerar en lista med textchunkar.
55
- Om chunk_size <= 0, returneras texten som en enda bit.
56
- """
57
- if chunk_size <= 0:
58
- return [text]
59
- # Använder textwrap.wrap eller en egen slicing:
60
- wrapped = textwrap.wrap(text, width=chunk_size)
61
- return wrapped
62
-
63
- def build_faq_embeddings(df: pd.DataFrame, chunk_size: int):
64
- """
65
- Bygger en lista med (fråga, svar, kategori) men chunkar
66
- svaret om chunk_size > 0. Returnerar:
67
- - en DataFrame expanded_df
68
- - embeddings (tensor)
69
- Varje "rad" i expanded_df representerar en chunk av svaret.
70
- """
71
- all_rows = []
72
- for _, row in df.iterrows():
73
- fråga = row["Fråga"]
74
- svar = row["Svar"]
75
- kategori = row["Kategori"]
76
- # Chunka svaret
77
- svar_chunks = chunk_text(str(svar), chunk_size)
78
- # Skapa en ny rad för varje chunk
79
- for chunk in svar_chunks:
80
- all_rows.append((fråga, chunk, kategori))
81
-
82
- expanded_df = pd.DataFrame(all_rows, columns=["Fråga", "Svar_chunk", "Kategori"])
83
- # Nu skapar vi embeddings baserat på "Fråga + Svar_chunk" eller bara "Fråga"?
84
- # Här visar vi ett exempel där vi enbart använder FAQ-frågan som embed.
85
- # Om du vill även ta med chunkade svaret i embeddings kan du ändra här.
86
- faq_embeddings = model.encode(expanded_df["Fråga"].tolist(), convert_to_tensor=True)
87
- return expanded_df, faq_embeddings
88
-
89
- # Bygg initial embeddings
90
- expanded_df, faq_embeddings = build_faq_embeddings(df, CHUNK_SIZE)
91
-
92
- # ====================================================
93
- # FUNKTIONER FÖR SÖK & FAQ-HANTERING
94
- # ====================================================
95
- def uppdatera_parametrar(chunk_size, temperature):
96
- """
97
- Uppdaterar våra globala parametrar för chunkning och "temperature".
98
- Bygger om embeddings om chunk_size ändras.
99
- """
100
- global CHUNK_SIZE, TEMPERATURE, df, expanded_df, faq_embeddings
101
-
102
- # Sätt nya värden
103
- CHUNK_SIZE = int(chunk_size)
104
- TEMPERATURE = float(temperature)
105
-
106
- # Bygg om embeddings med nya chunk_size
107
- expanded_df, faq_embeddings = build_faq_embeddings(df, CHUNK_SIZE)
108
-
109
- return f"Parametrar uppdaterade:\n- CHUNK_SIZE={CHUNK_SIZE}\n- TEMPERATURE={TEMPERATURE}"
110
-
111
- def sök_faq(question, top_k, threshold):
112
- """
113
- Söker i expanded_df med semantisk likhet.
114
- 'temperature' används här som en "slump-faktor" (demonstration).
115
- - En låg temperature -> sortera normalt efter cos sim
116
- - En högre temperature -> slumpa lite i top-k
117
- Returnerar en DataFrame med max top_k rader.
118
- """
119
- question = question.strip()
120
- if not question:
121
- return pd.DataFrame(columns=["Liknande FAQ-fråga", "Svar_chunk", "Kategori", "Confidence"])
122
-
123
- # Skapa embedding för query
124
- query_emb = model.encode(question, convert_to_tensor=True)
125
- cos_scores = util.cos_sim(query_emb, faq_embeddings)[0]
126
-
127
- # Hämta index och scores
128
- # Ex: top_k = 5
129
- top_results = torch.topk(cos_scores, k=int(top_k))
130
- indices = top_results.indices.tolist()
131
- scores = top_results.values.tolist()
132
-
133
- # Om TEMPERATURE > 0, "skaka om" listan lite (demonstrationssyfte).
134
- # Ju högre temperatur, desto större slumpmoment i ordningen.
135
- # (OBS: Detta är inte en "officiell" generativ temperatur-implementation,
136
- # men visar hur man kan introducera variation.)
137
- if TEMPERATURE > 0:
138
- import random
139
- pairs = list(zip(indices, scores))
140
- # slumpa om med en vikt av 'TEMPERATURE'
141
- # t.ex. multiplicera score med en slumpfaktor
142
- new_pairs = []
143
- for (idx, sc) in pairs:
144
- random_factor = 1.0 + (random.random() - 0.5) * TEMPERATURE
145
- new_pairs.append((idx, sc * random_factor))
146
- # sortera om
147
- new_pairs.sort(key=lambda x: x[1], reverse=True)
148
- indices = [p[0] for p in new_pairs]
149
- scores = [p[1] for p in new_pairs]
150
-
151
- data = []
152
- for i, (idx, score) in enumerate(zip(indices, scores)):
153
- if score >= threshold:
154
- row = expanded_df.iloc[idx]
155
- data.append({
156
- "Liknande FAQ-fråga": row["Fråga"],
157
- "Svar_chunk": row["Svar_chunk"],
158
- "Kategori": row["Kategori"],
159
- "Confidence": round(float(score), 3)
160
- })
161
-
162
- # Begränsa faktiskt antal rader till top_k
163
- data = data[:int(top_k)]
164
- results_df = pd.DataFrame(data)
165
- return results_df
166
-
167
- def hitta_topp3_förslag(fråga):
168
- """
169
- En enkel variant som alltid hämtar top_k=3, threshold=0.0
170
- (används i de snabba sökexemplen).
171
- """
172
- return sök_faq(fråga, top_k=3, threshold=0.0)
173
-
174
- def lägg_till_faq(fråga, svar, kategori):
175
- """
176
- Lägger till en ny rad i FAQ-DataFrame och pushar ändringen.
177
- """
178
- global df, expanded_df, faq_embeddings
179
-
180
- fråga = fråga.strip()
181
- svar = svar.strip()
182
- kategori = kategori.strip()
183
-
184
- if not fråga or not svar or not kategori:
185
- return "Fråga, svar och kategori får inte vara tomma!"
186
-
187
- try:
188
- # Lägg till rad
189
- ny_rad = pd.DataFrame([[fråga, svar, kategori]], columns=["Fråga", "Svar", "Kategori"])
190
- df = pd.concat([df, ny_rad], ignore_index=True)
191
-
192
- # Spara CSV
193
- df.to_csv(faq_path, index=False, sep=";")
194
-
195
- # Bygg nya embeddings med nya df
196
- expanded_df, faq_embeddings = build_faq_embeddings(df, CHUNK_SIZE)
197
-
198
- # Git commit & push
199
- repo.git_add()
200
- repo.git_commit(f"Lade till FAQ: {fråga[:50]}...")
201
- repo.git_push()
202
-
203
- return "Fråga tillagd och synkad med Hugging Face!"
204
- except Exception as e:
205
- return f"Fel vid uppdatering: {str(e)}"
206
-
207
- def visa_senaste_faq(antal=10):
208
- """Returnerar de sista N raderna från FAQ-DataFrame."""
209
- return df.tail(antal)
210
-
211
- def uppdatera_faq(gammal_fråga, nytt_svar, ny_kategori):
212
- """
213
- Uppdaterar en befintlig rad (matchar på 'gammal_fråga').
214
- """
215
- global df, expanded_df, faq_embeddings
216
- gammal_fråga = gammal_fråga.strip()
217
- nytt_svar = nytt_svar.strip()
218
- ny_kategori = ny_kategori.strip()
219
-
220
- if not gammal_fråga:
221
- return "Ingen fråga vald."
222
-
223
- match_index = df.index[df["Fråga"] == gammal_fråga]
224
- if len(match_index) == 0:
225
- return "Ingen matchande FAQ-fråga hittad."
226
-
227
- # Uppdatera
228
- if nytt_svar:
229
- df.loc[match_index, "Svar"] = nytt_svar
230
- if ny_kategori:
231
- df.loc[match_index, "Kategori"] = ny_kategori
232
-
233
- # Spara & uppdatera embeddings
234
- try:
235
- df.to_csv(faq_path, index=False, sep=";")
236
- expanded_df, faq_embeddings = build_faq_embeddings(df, CHUNK_SIZE)
237
- repo.git_add()
238
- repo.git_commit(f"Uppdaterade FAQ: {gammal_fråga[:50]}...")
239
- repo.git_push()
240
- return f"FAQ uppdaterad: {gammal_fråga}"
241
- except Exception as e:
242
- return f"Fel vid uppdatering: {str(e)}"
243
-
244
- def ta_bort_faq(fråga_att_radera):
245
- """
246
- Tar bort en FAQ-rad baserat på exakt match på 'fråga'.
247
- """
248
- global df, expanded_df, faq_embeddings
249
- fråga_att_radera = fråga_att_radera.strip()
250
- if not fråga_att_radera:
251
- return "Ingen fråga vald."
252
-
253
- match_index = df.index[df["Fråga"] == fråga_att_radera]
254
- if len(match_index) == 0:
255
- return "Ingen matchande FAQ-fråga hittad."
256
-
257
- # Ta bort rad
258
- df.drop(match_index, inplace=True)
259
-
260
- # Spara, uppdatera embeddings
261
- try:
262
- df.to_csv(faq_path, index=False, sep=";")
263
- expanded_df, faq_embeddings = build_faq_embeddings(df, CHUNK_SIZE)
264
- repo.git_add()
265
- repo.git_commit(f"Raderade FAQ: {fråga_att_radera[:50]}...")
266
- repo.git_push()
267
- return f"FAQ borttagen: {fråga_att_radera}"
268
- except Exception as e:
269
- return f"Fel vid borttagning: {str(e)}"
270
-
271
-
272
- # ====================================================
273
- # GRADIO-GRÄNSSNITT
274
- # ====================================================
275
- with gr.Blocks() as demo:
276
- gr.Markdown("# ChargeNode Chatbot – Utvecklarläge\n"
277
- "Ett admin-UI där du kan hantera FAQ-poster, testa sökning och "
278
- "leka med parametrar (chunk size & \"heat\").")
279
-
280
- # =======================
281
- # (A) Parametrar
282
- # =======================
283
- with gr.Accordion("Parametrar", open=True):
284
- gr.Markdown("Justera chunk-storlek och \"temperature\" (heat).")
285
- chunk_slider = gr.Slider(minimum=0, maximum=200, step=10, value=0,
286
- label="CHUNK_SIZE (tecken per bit). 0 = av.")
287
- temp_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=0.0,
288
- label="TEMPERATURE (heat). 0 = ingen slump, 1 = max slump.")
289
- uppdatera_btn = gr.Button("Uppdatera parametrar")
290
- param_output = gr.Textbox(label="Status")
291
-
292
- uppdatera_btn.click(
293
- fn=uppdatera_parametrar,
294
- inputs=[chunk_slider, temp_slider],
295
- outputs=param_output
296
- )
297
-
298
- # =======================
299
- # (B) SÖK I FAQ
300
- # =======================
301
- with gr.Accordion("Sök i FAQ", open=False):
302
- gr.Markdown("Skriv in en fråga och se topp K träffar.")
303
- fråga_input = gr.Textbox(label="Din fråga", placeholder="Ex: Var finns närmaste laddstation?")
304
- topk_slider = gr.Slider(minimum=1, maximum=10, step=1, value=3, label="Antal toppträffar (top_k)")
305
- threshold_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=0.0, label="Confidence threshold")
306
- sök_knapp = gr.Button("Sök")
307
- sök_result = gr.Dataframe(headers=["Liknande FAQ-fråga", "Svar_chunk", "Kategori", "Confidence"],
308
- label="Sökresultat")
309
-
310
- sök_knapp.click(
311
- fn=sök_faq,
312
- inputs=[fråga_input, topk_slider, threshold_slider],
313
- outputs=sök_result
314
- )
315
-
316
- # =======================
317
- # (C) KORTVARIANTE: Hitta topp 3
318
- # =======================
319
- with gr.Accordion("Enkel sökning (topp 3)", open=False):
320
- gr.Markdown("Här kan du snabbt få topp 3 träffar (threshold=0) utan att ställa in parametrar.")
321
- fråga_input2 = gr.Textbox(label="Din fråga", placeholder="Ex: Hur ändrar jag lösenord?")
322
- knapp2 = gr.Button("Hämta topp 3")
323
- result2 = gr.Dataframe(label="Topp 3 förslag")
324
- knapp2.click(fn=hitta_topp3_förslag, inputs=fråga_input2, outputs=result2)
325
-
326
- # =======================
327
- # (D) Lägg till ny FAQ-rad
328
- # =======================
329
- with gr.Accordion("Lägg till en ny FAQ-rad", open=False):
330
- ny_fråga = gr.Textbox(label="Ny fråga", placeholder="Ex: Hur registrerar jag ett konto?")
331
- nytt_svar = gr.Textbox(label="Nytt svar", placeholder="Skriv ditt svar här...")
332
- ny_kategori = gr.Textbox(label="Ny kategori", placeholder="Ex: Konto")
333
-
334
- lägg_till_knapp = gr.Button("Lägg till i FAQ")
335
- lägg_till_output = gr.Textbox(label="Status")
336
-
337
- def clear_add_fields():
338
- return "", "", ""
339
-
340
- reset_ny = gr.Button("Rensa fält")
341
-
342
- lägg_till_knapp.click(
343
- fn=lägg_till_faq,
344
- inputs=[ny_fråga, nytt_svar, ny_kategori],
345
- outputs=lägg_till_output
346
- )
347
- reset_ny.click(
348
- fn=clear_add_fields,
349
- inputs=[],
350
- outputs=[ny_fråga, nytt_svar, ny_kategori]
351
- )
352
-
353
- # =======================
354
- # (E) Redigera/ta bort FAQ
355
- # =======================
356
- with gr.Accordion("Redigera eller ta bort befintlig FAQ", open=False):
357
- befintliga_frågor = gr.Dropdown(choices=df["Fråga"].tolist(),
358
- label="Välj en befintlig fråga")
359
- nytt_svar_edit = gr.Textbox(label="Nytt svar (valfritt)")
360
- ny_kategori_edit = gr.Textbox(label="Ny kategori (valfritt)")
361
- uppdatera_btn2 = gr.Button("Uppdatera vald FAQ")
362
- uppdatera_output = gr.Textbox(label="Status")
363
-
364
- radera_btn = gr.Button("Ta bort vald FAQ")
365
- radera_output = gr.Textbox(label="Status")
366
-
367
- uppdatera_btn2.click(
368
- fn=uppdatera_faq,
369
- inputs=[befintliga_frågor, nytt_svar_edit, ny_kategori_edit],
370
- outputs=uppdatera_output
371
- )
372
- radera_btn.click(
373
- fn=ta_bort_faq,
374
- inputs=befintliga_frågor,
375
- outputs=radera_output
376
- )
377
-
378
- # =======================
379
- # (F) Visa senaste FAQ-poster
380
- # =======================
381
- with gr.Accordion("Visa senaste FAQ-poster", open=False):
382
- visa_btn = gr.Button("Visa senaste 10")
383
- logg_output = gr.Dataframe(label="Sista 10 FAQ-rader")
384
- visa_btn.click(fn=visa_senaste_faq, inputs=[], outputs=logg_output)
385
-
386
- gr.Markdown("---")
387
- gr.Markdown("**OBS:** Detta skript använder en \"Repository\"-klass som är på väg att fasas ut "
388
- "i huggingface_hub. Det fungerar i dagsläget, men vill du framöver undvika detta "
389
- "kan du gå över till HTTP-baserade metoder för att ladda upp/ned filer. "
390
- "Se Hugging Face-dokumentationen för mer info.")
391
-
392
- # Starta Gradio
393
- if __name__ == "__main__":
394
- demo.launch()
 
1
+ import os
2
+ import pandas as pd
3
+ import torch
4
+ import gradio as gr
5
+
6
+ from huggingface_hub import login, Repository
7
+ from sentence_transformers import SentenceTransformer, util
8
+
9
+ # ================================
10
+ # KONFIG & INIT
11
+ # ================================
12
+ HF_TOKEN = os.getenv("HUGGINGFACE_TOKEN")
13
+ if not HF_TOKEN:
14
+ raise ValueError("HUGGINGFACE_TOKEN miljövariabel saknas!")
15
+
16
+ login(token=HF_TOKEN)
17
+
18
+ REPO_ID = "ChargeNodeEurope/Chatbot_4o_mini" # Exempel
19
+ REPO_LOCAL_PATH = "chatbot_faq_repo"
20
+ FAQ_CSV_PATH = "faq_städad_utf8_fixed.csv"
21
+
22
+ repo = Repository(
23
+ local_dir=REPO_LOCAL_PATH,
24
+ clone_from=REPO_ID,
25
+ repo_type="space", # Viktigt om det är en Space
26
+ use_auth_token=HF_TOKEN
27
+ )
28
+
29
+ faq_path = os.path.join(REPO_LOCAL_PATH, FAQ_CSV_PATH)
30
+ try:
31
+ df = pd.read_csv(faq_path, sep=";")
32
+ df.dropna(subset=["Fråga", "Svar"], inplace=True)
33
+ except Exception as e:
34
+ raise FileNotFoundError(f"Kunde inte ladda FAQ-filen: {str(e)}")
35
+
36
+ # Ladda Sentence Transformer
37
+ model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
38
+
39
+ # Skapa embeddings av alla FAQ-frågor en gång
40
+ faq_questions = df["Fråga"].tolist()
41
+ faq_embeddings = model.encode(faq_questions, convert_to_tensor=True)
42
+
43
+
44
+ # ================================
45
+ # HJÄLPFUNKTIONER
46
+ # ================================
47
+ def uppdatera_embeddings():
48
+ """
49
+ Uppdatera embeddings efter att FAQ-DataFrame har ändrats.
50
+ """
51
+ global faq_questions, faq_embeddings, df
52
+ faq_questions = df["Fråga"].tolist()
53
+ faq_embeddings = model.encode(faq_questions, convert_to_tensor=True)
54
+
55
+ def sök_faq(fråga):
56
+ """
57
+ Gör en enkel semantisk sökning i FAQ och returnerar topp 3 resultat.
58
+ """
59
+ fråga = fråga.strip()
60
+ if not fråga:
61
+ return pd.DataFrame(columns=["Liknande fråga", "Svar", "Kategori", "Confidence"])
62
+
63
+ # Skapa embedding för query
64
+ query_emb = model.encode(fråga, convert_to_tensor=True)
65
+ cos_scores = util.cos_sim(query_emb, faq_embeddings)[0]
66
+
67
+ # Hämta topp 3
68
+ top_results = torch.topk(cos_scores, k=3)
69
+ indices = top_results.indices.tolist()
70
+ scores = top_results.values.tolist()
71
+
72
+ data = []
73
+ for idx, score in zip(indices, scores):
74
+ row = df.iloc[idx]
75
+ data.append({
76
+ "Liknande fråga": row["Fråga"],
77
+ "Svar": row["Svar"],
78
+ "Kategori": row["Kategori"],
79
+ "Confidence": round(float(score), 3)
80
+ })
81
+ return pd.DataFrame(data)
82
+
83
+ def lägg_till_faq(fråga, svar, kategori):
84
+ """
85
+ Lägger till en ny fråga i df, sparar och pushar, uppdaterar embeddings.
86
+ """
87
+ global df
88
+ fråga = fråga.strip()
89
+ svar = svar.strip()
90
+ kategori = kategori.strip()
91
+
92
+ if not fråga or not svar or not kategori:
93
+ return "Fråga, svar och kategori får inte vara tomma!"
94
+
95
+ try:
96
+ # Lägg till i DataFrame
97
+ ny_rad = pd.DataFrame([[fråga, svar, kategori]], columns=["Fråga", "Svar", "Kategori"])
98
+ df = pd.concat([df, ny_rad], ignore_index=True)
99
+
100
+ # Spara och pusha
101
+ df.to_csv(faq_path, index=False, sep=";")
102
+ uppdatera_embeddings() # Skapa nya embeddings
103
+
104
+ repo.git_add()
105
+ repo.git_commit(f"Lade till FAQ: {fråga[:50]}...")
106
+ repo.git_push()
107
+
108
+ return "Fråga tillagd och synkad med Hugging Face!"
109
+ except Exception as e:
110
+ return f"Fel vid uppdatering: {str(e)}"
111
+
112
+ def visa_senaste_faq(antal=10):
113
+ return df.tail(antal)
114
+
115
+ def uppdatera_faq(gammal_fråga, nytt_svar, ny_kategori):
116
+ global df
117
+ gammal_fråga = gammal_fråga.strip()
118
+ nytt_svar = nytt_svar.strip()
119
+ ny_kategori = ny_kategori.strip()
120
+
121
+ if not gammal_fråga:
122
+ return "Ingen fråga vald."
123
+
124
+ match_index = df.index[df["Fråga"] == gammal_fråga]
125
+ if len(match_index) == 0:
126
+ return "Ingen matchande FAQ-fråga hittad."
127
+
128
+ if nytt_svar:
129
+ df.loc[match_index, "Svar"] = nytt_svar
130
+ if ny_kategori:
131
+ df.loc[match_index, "Kategori"] = ny_kategori
132
+
133
+ try:
134
+ df.to_csv(faq_path, index=False, sep=";")
135
+ uppdatera_embeddings()
136
+
137
+ repo.git_add()
138
+ repo.git_commit(f"Uppdaterade FAQ: {gammal_fråga[:50]}...")
139
+ repo.git_push()
140
+ return "FAQ uppdaterad!"
141
+ except Exception as e:
142
+ return f"Fel vid uppdatering: {str(e)}"
143
+
144
+ def ta_bort_faq(fråga_att_radera):
145
+ global df
146
+ fråga_att_radera = fråga_att_radera.strip()
147
+ if not fråga_att_radera:
148
+ return "Ingen fråga vald."
149
+
150
+ match_index = df.index[df["Fråga"] == fråga_att_radera]
151
+ if len(match_index) == 0:
152
+ return "Ingen matchande FAQ-fråga hittad."
153
+
154
+ df.drop(match_index, inplace=True)
155
+
156
+ try:
157
+ df.to_csv(faq_path, index=False, sep=";")
158
+ uppdatera_embeddings()
159
+
160
+ repo.git_add()
161
+ repo.git_commit(f"Raderade FAQ: {fråga_att_radera[:50]}...")
162
+ repo.git_push()
163
+ return f"FAQ borttagen: {fråga_att_radera}"
164
+ except Exception as e:
165
+ return f"Fel vid borttagning: {str(e)}"
166
+
167
+
168
+ # ================================
169
+ # GRADIO-GRÄNSSNITT
170
+ # ================================
171
+ with gr.Blocks() as demo:
172
+ gr.Markdown("# Enkel FAQ Admin")
173
+ gr.Markdown("Administrera FAQ-poster, sök i befintliga frågor, lägg till, uppdatera eller ta bort.")
174
+
175
+ # ---- Sök i FAQ ----
176
+ gr.Markdown("## Sök i FAQ")
177
+ inp_question = gr.Textbox(label="Din fråga", placeholder="Ex: Hur startar jag en laddning?")
178
+ btn_search = gr.Button("Sök")
179
+ out_search = gr.Dataframe(label="Topp 3 resultat")
180
+
181
+ btn_search.click(fn=sök_faq, inputs=inp_question, outputs=out_search)
182
+
183
+ # ---- Lägg till FAQ ----
184
+ gr.Markdown("## Lägg till FAQ")
185
+ add_question = gr.Textbox(label="Ny fråga")
186
+ add_answer = gr.Textbox(label="Nytt svar")
187
+ add_cat = gr.Textbox(label="Ny kategori")
188
+ btn_add = gr.Button("Lägg till")
189
+ out_add = gr.Textbox(label="Status")
190
+ btn_add.click(fn=lägg_till_faq, inputs=[add_question, add_answer, add_cat], outputs=out_add)
191
+
192
+ # ---- Redigera / Ta bort ----
193
+ gr.Markdown("## Redigera / Ta bort FAQ")
194
+ existing_quests = gr.Dropdown(choices=df["Fråga"].tolist(), label="Befintliga frågor")
195
+ new_answer = gr.Textbox(label="Nytt svar (valfritt)")
196
+ new_cat = gr.Textbox(label="Ny kategori (valfritt)")
197
+ btn_update = gr.Button("Uppdatera")
198
+ out_update = gr.Textbox(label="Status")
199
+
200
+ btn_delete = gr.Button("Ta bort")
201
+ out_delete = gr.Textbox(label="Status")
202
+
203
+ btn_update.click(fn=uppdatera_faq, inputs=[existing_quests, new_answer, new_cat], outputs=out_update)
204
+ btn_delete.click(fn=ta_bort_faq, inputs=existing_quests, outputs=out_delete)
205
+
206
+ # ---- Visa senaste ----
207
+ gr.Markdown("## Visa senaste FAQ-poster")
208
+ btn_show = gr.Button("Visa senaste 10")
209
+ out_log = gr.Dataframe(label="Senaste poster")
210
+ btn_show.click(fn=visa_senaste_faq, inputs=[], outputs=out_log)
211
+
212
+ if __name__ == "__main__":
213
+ demo.launch()