limloop commited on
Commit
a8a7773
·
1 Parent(s): c441560

добавил языки

Browse files
Files changed (8) hide show
  1. README.md +38 -1
  2. app.py +20 -4
  3. locales.py +19 -0
  4. locales/en.json +32 -0
  5. locales/ru.json +32 -0
  6. requirements.txt +1 -0
  7. ui_components.py +66 -148
  8. utils.py +6 -6
README.md CHANGED
@@ -11,6 +11,43 @@ license: mit
11
  ---
12
 
13
  # 🧙🌀 Fantasy Characters Explorer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  **Интерактивная библиотека персонажей для RPG и вдохновения**
15
 
16
  [![Open in Spaces](https://img.shields.io/badge/🤗-Open%20in%20Spaces-blue)](https://huggingface.co/spaces/loim/ru_fantasy_characters_explorer)
@@ -26,7 +63,7 @@ license: mit
26
  ## 🎮 Как использовать
27
  1. Выберите фильтры
28
  2. Нажмите на персонажа, чтобы раскрыть детали
29
- 3. Скопируйте код для RPG-сессий через иконку 📋
30
  4. Вдохновляйтесь и создавайте истории!
31
 
32
  ## 🗝️ Пример персонажа
 
11
  ---
12
 
13
  # 🧙🌀 Fantasy Characters Explorer
14
+ # English
15
+ **Interactive Character Library for RPGs and Inspiration**
16
+
17
+ [![Open in Spaces](https://img.shields.io/badge/🤗-Open%20in%20Spaces-blue)](https://huggingface.co/spaces/loim/ru_fantasy_characters_explorer)
18
+ [![Dataset](https://img.shields.io/badge/🗃️-Original_Dataset-ff69b4)](https://huggingface.co/datasets/loim/ru_fantasy_characters)
19
+
20
+ ## ✨ Features
21
+ - **Search** by name, description, or tags
22
+ - **Filters**: absurdity, mood, tags
23
+ - **RPG character cards** in Markdown (one-click copy)
24
+ - **Sorting** by name/absurdity level
25
+ - Atmospheric design
26
+
27
+ ## 🎮 How to Use
28
+ 1. Apply filters
29
+ 2. Click on a character to expand details
30
+ 3. Copy the RPG-ready code
31
+ 4. Get inspired and craft stories!
32
+
33
+ ## 🗝️ Example Character
34
+ ```markdown
35
+ **Name:** Каландра Сольна Губа
36
+ **World:** Омут Шепчущих Волн — океан, где вода тверже стали...
37
+ **Mood:** Мрачный
38
+ **Absurdity:** 7/10
39
+
40
+ **Description:** Женщина с чешуйчатой кожей цвета шторма...
41
+ > *«Твой корабль... уже дал течь...»*
42
+ ```
43
+
44
+ ## 🛠️ Technologies
45
+ `Python` · `Streamlit` · `Hugging Face Datasets`
46
+
47
+ ---
48
+ 🔮 *Created for those who seek magic in the details*
49
+
50
+ # Русский
51
  **Интерактивная библиотека персонажей для RPG и вдохновения**
52
 
53
  [![Open in Spaces](https://img.shields.io/badge/🤗-Open%20in%20Spaces-blue)](https://huggingface.co/spaces/loim/ru_fantasy_characters_explorer)
 
63
  ## 🎮 Как использовать
64
  1. Выберите фильтры
65
  2. Нажмите на персонажа, чтобы раскрыть детали
66
+ 3. Скопируйте код для RPG-сессий
67
  4. Вдохновляйтесь и создавайте истории!
68
 
69
  ## 🗝️ Пример персонажа
app.py CHANGED
@@ -1,26 +1,42 @@
1
  import streamlit as st
2
- from data_loader import load_data, get_unique_mood, get_unique_tags
3
  from utils import apply_filters
 
 
4
  from ui_components import render_sidebar, render_main_content, load_css
 
 
 
 
5
 
6
  # Настройки страницы
7
  st.set_page_config(layout="wide", page_icon="🧙", page_title="Fantasy Characters Explorer")
 
8
 
9
  # Загрузка данных
10
  df = load_data()
11
 
 
 
 
 
 
 
 
 
12
  # Загрузка стилей
13
  st.markdown(f"<style>{load_css()}</style>", unsafe_allow_html=True)
14
 
15
  # Получение уникальных значений для фильтров
16
- moods = ["Все"] + get_unique_mood(df)
17
  all_tags = get_unique_tags(df)
18
 
19
  # Рендеринг сайдбара с фильтрами
20
- selected_filters = render_sidebar(moods, all_tags)
21
 
22
  # Применение фильтров
23
  filtered_df = apply_filters(
 
24
  df=df,
25
  search_query=selected_filters["search_query"],
26
  absurdity_range=selected_filters["absurdity_range"],
@@ -30,4 +46,4 @@ filtered_df = apply_filters(
30
  )
31
 
32
  # Основной контент
33
- render_main_content(filtered_df)
 
1
  import streamlit as st
2
+ from streamlit_cookies_controller import CookieController
3
  from utils import apply_filters
4
+ from locales import load_locale, lang_codes
5
+ from data_loader import load_data, get_unique_mood, get_unique_tags
6
  from ui_components import render_sidebar, render_main_content, load_css
7
+ import time
8
+
9
+ DEFAULT_LANG = "en"
10
+
11
 
12
  # Настройки страницы
13
  st.set_page_config(layout="wide", page_icon="🧙", page_title="Fantasy Characters Explorer")
14
+ cookie = CookieController()
15
 
16
  # Загрузка данных
17
  df = load_data()
18
 
19
+ st.session_state.lang = cookie.get("lang")
20
+ all_langs = lang_codes()
21
+ if st.session_state.lang not in all_langs:
22
+ st.session_state.lang = (st.context.locale.split("-", 1)[0] if st.context.locale else DEFAULT_LANG)
23
+ cookie.set("lang", st.session_state.lang)
24
+
25
+ locale = load_locale(st.session_state.lang)
26
+
27
  # Загрузка стилей
28
  st.markdown(f"<style>{load_css()}</style>", unsafe_allow_html=True)
29
 
30
  # Получение уникальных значений для фильтров
31
+ moods = [locale["filter_all"]] + get_unique_mood(df)
32
  all_tags = get_unique_tags(df)
33
 
34
  # Рендеринг сайдбара с фильтрами
35
+ selected_filters = render_sidebar(cookie, locale, all_langs, moods, all_tags)
36
 
37
  # Применение фильтров
38
  filtered_df = apply_filters(
39
+ locale,
40
  df=df,
41
  search_query=selected_filters["search_query"],
42
  absurdity_range=selected_filters["absurdity_range"],
 
46
  )
47
 
48
  # Основной контент
49
+ render_main_content(locale, filtered_df)
locales.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ import streamlit as st
4
+
5
+ locales_dir = Path("locales")
6
+
7
+ @st.cache_data
8
+ def lang_codes():
9
+ return [l.stem for l in locales_dir.glob("*.json")]
10
+
11
+ @st.cache_data
12
+ def load_locale(lang):
13
+ locale = {}
14
+
15
+ locale_dir = locales_dir / f"{lang}.json"
16
+ with open(locale_dir, "r", encoding="utf-8") as f:
17
+ locale = json.load(f)
18
+
19
+ return locale
locales/en.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "filter_lang": "Languages",
3
+ "filter_all": "All",
4
+ "filter_title": "🔮 Filters",
5
+ "filter_search": "🔍 Search by name/description",
6
+ "filter_sort_title": "Sorting",
7
+ "filter_sort": ["By name (A→Z)", "By name (Z→A)", "By absurdity (↑)", "By absurdity (↓)"],
8
+ "filter_absurd": "Absurdity level",
9
+ "filter_mood": "Mood",
10
+ "filter_tags": "Tags",
11
+ "main_not_found": "No characters found. Try adjusting the filters.",
12
+ "main_counter": "Characters per page",
13
+ "main_page": "Page",
14
+ "main_found": "**Characters found:** {} (Page {}/{})",
15
+ "pages_first": "⏪ First",
16
+ "pages_before": "◀️ Back",
17
+ "pages_next": "Next ▶️",
18
+ "pages_last": "Last ⏩",
19
+ "char_world": "🌍 World:",
20
+ "char_mood": "🎭 Mood:",
21
+ "char_absurd": "🌀 Absurdity:",
22
+ "char_tags": "🏷️ Tags:",
23
+ "char_desc": "📖 Description:",
24
+ "char_style": "💬 Speech style:",
25
+ "char_rpg_card": "📋 RPG card",
26
+ "rpt_card_world": "World:",
27
+ "rpt_card_name": "Name:",
28
+ "rpt_card_desc": "Appearance:",
29
+ "rpt_card_story": "Personality:",
30
+ "rpt_card_style": "Speech:",
31
+ "rpt_card_msg": "First message:"
32
+ }
locales/ru.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "filter_lang": "Языки",
3
+ "filter_all": "Все",
4
+ "filter_title": "🔮 Фильтры",
5
+ "filter_search": "🔍 Поиск по имени/описанию",
6
+ "filter_sort_title": "Сортировка",
7
+ "filter_sort": ["По имени (А→Я)", "По имени (Я→А)", "По абсурдности (↑)", "По абсурдности (↓)"],
8
+ "filter_absurd": "Уровень абсурдности",
9
+ "filter_mood": "Настроение",
10
+ "filter_tags": "Теги",
11
+ "main_not_found": "Персонажи не найдены. Попробуйте изменить фильтры.",
12
+ "main_counter": "Персонажей на странице",
13
+ "main_page": "Страница",
14
+ "main_found": "**Найдено персонажей:** {} (Страница {}/{})",
15
+ "pages_first": "⏪ Начало",
16
+ "pages_before": "◀️ Назад",
17
+ "pages_next": "Вперед ▶️",
18
+ "pages_last": "Конец ⏩",
19
+ "char_world": "🌍 Мир:",
20
+ "char_mood": "🎭 Настроение:",
21
+ "char_absurd": "🌀 Абсурдность:",
22
+ "char_tags": "🏷️ Теги:",
23
+ "char_desc": "📖 Описание:",
24
+ "char_style": "💬 Стиль речи:",
25
+ "char_rpg_card": "📋 RPG-карточка",
26
+ "rpt_card_world": "Мир:",
27
+ "rpt_card_name": "Имя:",
28
+ "rpt_card_desc": "Внешность:",
29
+ "rpt_card_story": "Характер:",
30
+ "rpt_card_style": "Речь:",
31
+ "rpt_card_msg": "Первая реплика:"
32
+ }
requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
  streamlit
 
2
  datasets
3
  pandas
 
1
  streamlit
2
+ streamlit_cookies_controller
3
  datasets
4
  pandas
ui_components.py CHANGED
@@ -6,7 +6,7 @@ def load_css():
6
  with open("assets/styles.css") as f:
7
  return f.read()
8
 
9
- def render_sidebar(moods, all_tags):
10
  """Рендеринг боковой панели с фильтрами"""
11
  with st.sidebar:
12
  st.markdown(
@@ -15,15 +15,30 @@ def render_sidebar(moods, all_tags):
15
  https://huggingface.co/datasets/loim/ru_fantasy_characters)
16
  """
17
  )
18
- st.title("🔮 Фильтры")
19
- search_query = st.text_input("🔍 Поиск по имени/описанию", "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  sort_option = st.selectbox(
21
- "Сортировка",
22
- ["По имени (А→Я)", "По имени (Я→А)", "По абсурдности (↑)", "По абсурдности (↓)"]
23
  )
24
- absurdity_range = st.slider("Уровень абсурдности", 0, 10, (0, 10))
25
- selected_mood = st.selectbox("Настроение", moods)
26
- selected_tags = st.multiselect("Теги", all_tags)
27
 
28
  return {
29
  "search_query": search_query,
@@ -33,26 +48,51 @@ def render_sidebar(moods, all_tags):
33
  "selected_tags": selected_tags
34
  }
35
 
36
- def generate_rpg_card(row):
 
37
  """Генерация RPG-карточки персонажа"""
38
  return f"""
39
- **Мир:** {row['world']}
40
 
41
- **Имя:** {row['name']}
42
- **Внешность:** {row['description']}
43
- **Характер:** {row['short_story']}
44
- **Речь:** {row['style']}
45
 
46
- **Первая реплика:**
47
  > {row['first_message']}
48
  """
49
 
50
- def render_main_content(filtered_df):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  """Рендеринг основного контента с пагинацией"""
52
  st.title("🧙 Fantasy Characters Explorer")
53
 
54
  if len(filtered_df) == 0:
55
- st.warning("Персонажи не найдены. Попробуйте изменить фильтры.")
56
  else:
57
  # Инициализация состояния пагинации
58
  if 'page' not in st.session_state:
@@ -66,7 +106,7 @@ def render_main_content(filtered_df):
66
  col1, col2, _ = st.columns([2, 3, 5])
67
  with col1:
68
  st.selectbox(
69
- "Персонажей на странице",
70
  options=[5, 10, 20],
71
  index=1,
72
  key='per_page',
@@ -74,7 +114,7 @@ def render_main_content(filtered_df):
74
  )
75
  with col2:
76
  st.number_input(
77
- "Страница",
78
  min_value=0,
79
  max_value=total_pages-1,
80
  value=st.session_state.page,
@@ -88,151 +128,29 @@ def render_main_content(filtered_df):
88
  end_idx = min((st.session_state.page + 1) * st.session_state.per_page, len(filtered_df))
89
 
90
  # Отображение статистики
91
- st.markdown(f"**Найдено персонажей:** {len(filtered_df)} (Страница {st.session_state.page + 1}/{total_pages})")
92
-
93
- # Кэшированный рендеринг карточек
94
- @st.cache_data(max_entries=100)
95
- def render_character_card(row):
96
- with st.container():
97
- st.markdown(f"### {row['name']}")
98
- st.caption(f"*{row['short_story']}*")
99
-
100
- col1, col2 = st.columns([1, 3])
101
- with col1:
102
- st.markdown(f"**🌍 Мир:** {row['world']}")
103
- st.markdown(f"**🎭 Настроение:** `{row['mood']}`")
104
- st.markdown(f"**🌀 Абсурдность:** `{row['absurdity']}/10`")
105
- st.markdown(f"**🏷️ Теги:** `{' '.join(row['tags'].split())}`")
106
-
107
- with col2:
108
- st.markdown(f"**📖 Описание:** {row['description']}")
109
- st.markdown(f"**💬 Стиль речи:** *{row['style']}*")
110
-
111
- with st.expander("📋 RPG-карточка", expanded=False):
112
- rpg_card = generate_rpg_card(row)
113
- st.code(rpg_card, language="markdown")
114
-
115
- st.markdown("---")
116
 
117
  # Рендеринг только видимых карточек
118
  for idx in range(start_idx, end_idx):
119
  row = filtered_df.iloc[idx]
120
- render_character_card(row)
121
 
122
  # Кнопки навигации
123
  if total_pages > 1:
124
  cols = st.columns(4)
125
  with cols[0]:
126
- if st.button("⏪ Первая страница", disabled=(st.session_state.page == 0)):
127
  st.session_state.page = 0
128
  st.rerun()
129
  with cols[1]:
130
- if st.button("◀️ Назад", disabled=(st.session_state.page == 0)):
131
  st.session_state.page -= 1
132
  st.rerun()
133
  with cols[2]:
134
- if st.button("Вперед ▶️", disabled=(st.session_state.page >= total_pages-1)):
135
  st.session_state.page += 1
136
  st.rerun()
137
  with cols[3]:
138
- if st.button("Последняя ⏩", disabled=(st.session_state.page == total_pages-1)):
139
  st.session_state.page = total_pages-1
140
- st.rerun()
141
-
142
- def render_main_content(filtered_df):
143
- """Рендеринг основного контента с пагинацией"""
144
- st.title("🧙 Fantasy Characters Explorer")
145
-
146
- # Инициализация состояния пагинации
147
- if 'page' not in st.session_state:
148
- st.session_state.page = 0
149
-
150
- # Настройки пагинации
151
- per_page = st.session_state.get('per_page', 10)
152
- total_items = len(filtered_df)
153
- total_pages = max(1, (total_items + per_page - 1) // per_page)
154
-
155
- # Корректировка номера страницы при изменении данных
156
- if st.session_state.page >= total_pages:
157
- st.session_state.page = max(0, total_pages - 1)
158
-
159
- # Управление пагинацией в колонках
160
- col1, col2, _ = st.columns([2, 3, 5])
161
- with col1:
162
- st.selectbox(
163
- "Персонажей на странице",
164
- options=[5, 10, 20],
165
- index=1,
166
- key='per_page',
167
- on_change=lambda: st.session_state.update(page=0)
168
- )
169
- with col2:
170
- st.number_input(
171
- "Страница",
172
- min_value=0,
173
- max_value=max(total_pages-1, 0), # Защита от отрицательных значений
174
- value=min(st.session_state.page, total_pages-1), # Ограничение значения
175
- key='page_input',
176
- format="%d",
177
- on_change=lambda: setattr(st.session_state, 'page', st.session_state.page_input)
178
- )
179
-
180
- # Рассчет диапазона записей
181
- start_idx = st.session_state.page * per_page
182
- end_idx = min(start_idx + per_page, total_items)
183
-
184
- # Отображение статистики
185
- st.markdown(f"**Найдено персонажей:** {total_items} (Страница {st.session_state.page if total_items > 0 else 0}/{total_pages-1})")
186
-
187
- # Кэшированный рендеринг карточек
188
- @st.cache_data
189
- def render_character_card(row):
190
- with st.container():
191
- st.markdown(f"### {row['name']}")
192
- st.caption(f"*{row['short_story']}*")
193
-
194
- col1, col2 = st.columns([1, 3])
195
- with col1:
196
- st.markdown(f"**🌍 Мир:** {row['world']}")
197
- st.markdown(f"**🎭 Настроение:** `{row['mood']}`")
198
- st.markdown(f"**🌀 Абсурдность:** `{row['absurdity']}/10`")
199
- st.markdown(f"**🏷️ Теги:** `{' '.join(row['tags'].split())}`")
200
-
201
- with col2:
202
- st.markdown(f"**📖 Описание:** {row['description']}")
203
- st.markdown(f"**💬 Стиль речи:** *{row['style']}*")
204
-
205
- with st.expander("📋 RPG-карточка", expanded=False):
206
- rpg_card = generate_rpg_card(row)
207
- st.code(rpg_card, language="markdown")
208
-
209
- st.markdown("---")
210
-
211
- # Рендеринг только видимых карточек
212
- if total_items > 0:
213
- for idx in range(start_idx, end_idx):
214
- row = filtered_df.iloc[idx]
215
- render_character_card(row)
216
- else:
217
- st.warning("Персонажи не найдены. Попробуйте изменить фильтры.")
218
- return # Прерываем выполнение чтобы избежать ошибок
219
-
220
- # Кнопки навигации (только если есть результаты)
221
- if total_pages > 1:
222
- cols = st.columns(4)
223
- with cols[0]:
224
- if st.button("⏪ Первая страница", disabled=(st.session_state.page == 0)):
225
- st.session_state.page = 0
226
- st.rerun()
227
- with cols[1]:
228
- if st.button("◀️ Назад", disabled=(st.session_state.page == 0)):
229
- st.session_state.page -= 1
230
- st.rerun()
231
- with cols[2]:
232
- if st.button("Вперед ▶️", disabled=(st.session_state.page >= total_pages-1)):
233
- st.session_state.page += 1
234
- st.rerun()
235
- with cols[3]:
236
- if st.button("Последняя ⏩", disabled=(st.session_state.page == total_pages-1)):
237
- st.session_state.page = total_pages-1
238
- st.rerun()
 
6
  with open("assets/styles.css") as f:
7
  return f.read()
8
 
9
+ def render_sidebar(cookie, locale, all_langs, moods, all_tags):
10
  """Рендеринг боковой панели с фильтрами"""
11
  with st.sidebar:
12
  st.markdown(
 
15
  https://huggingface.co/datasets/loim/ru_fantasy_characters)
16
  """
17
  )
18
+
19
+ def on_language_change():
20
+ if st.session_state.lang_select != st.session_state.lang:
21
+ cookie.set("lang", st.session_state.lang_select)
22
+
23
+ st.selectbox(
24
+ locale["filter_lang"],
25
+ all_langs,
26
+ index=all_langs.index(st.session_state.lang),
27
+ on_change=on_language_change,
28
+ key="lang_select"
29
+ )
30
+
31
+ st.markdown("---")
32
+
33
+ st.title(locale["filter_title"])
34
+ search_query = st.text_input(locale["filter_search"], "")
35
  sort_option = st.selectbox(
36
+ locale["filter_sort_title"],
37
+ locale["filter_sort"]
38
  )
39
+ absurdity_range = st.slider(locale["filter_absurd"], 0, 10, (0, 10))
40
+ selected_mood = st.selectbox(locale["filter_mood"], moods)
41
+ selected_tags = st.multiselect(locale["filter_tags"], all_tags)
42
 
43
  return {
44
  "search_query": search_query,
 
48
  "selected_tags": selected_tags
49
  }
50
 
51
+ @st.cache_data
52
+ def generate_rpg_card(locale, row):
53
  """Генерация RPG-карточки персонажа"""
54
  return f"""
55
+ **{locale["rpt_card_world"]}** {row['world']}
56
 
57
+ **{locale["rpt_card_name"]}** {row['name']}
58
+ **{locale["rpt_card_desc"]}** {row['description']}
59
+ **{locale["rpt_card_story"]}** {row['short_story']}
60
+ **{locale["rpt_card_style"]}** {row['style']}
61
 
62
+ **{locale["rpt_card_msg"]}**
63
  > {row['first_message']}
64
  """
65
 
66
+ # Кэшированный рендеринг карточек
67
+ @st.cache_data
68
+ def render_character_card(locale, row):
69
+ with st.container():
70
+ st.markdown(f"### {row['name']}")
71
+ st.caption(f"*{row['short_story']}*")
72
+
73
+ col1, col2 = st.columns([1, 3])
74
+ with col1:
75
+ st.markdown(f"**{locale["char_world"]}** {row['world']}")
76
+ st.markdown(f"**{locale["char_mood"]}** `{row['mood']}`")
77
+ st.markdown(f"**{locale["char_absurd"]}** `{row['absurdity']}/10`")
78
+ st.markdown(f"**{locale["char_tags"]}** `{' '.join(row['tags'].split())}`")
79
+
80
+ with col2:
81
+ st.markdown(f"**{locale["char_desc"]}** {row['description']}")
82
+ st.markdown(f"**{locale["char_style"]}** *{row['style']}*")
83
+
84
+ with st.expander(locale["char_rpg_card"], expanded=False):
85
+ rpg_card = generate_rpg_card(locale, row)
86
+ st.code(rpg_card, language="markdown")
87
+
88
+ st.markdown("---")
89
+
90
+ def render_main_content(locale, filtered_df):
91
  """Рендеринг основного контента с пагинацией"""
92
  st.title("🧙 Fantasy Characters Explorer")
93
 
94
  if len(filtered_df) == 0:
95
+ st.warning(locale["main_not_found"])
96
  else:
97
  # Инициализация состояния пагинации
98
  if 'page' not in st.session_state:
 
106
  col1, col2, _ = st.columns([2, 3, 5])
107
  with col1:
108
  st.selectbox(
109
+ locale["main_counter"],
110
  options=[5, 10, 20],
111
  index=1,
112
  key='per_page',
 
114
  )
115
  with col2:
116
  st.number_input(
117
+ locale["main_page"],
118
  min_value=0,
119
  max_value=total_pages-1,
120
  value=st.session_state.page,
 
128
  end_idx = min((st.session_state.page + 1) * st.session_state.per_page, len(filtered_df))
129
 
130
  # Отображение статистики
131
+ st.markdown(locale["main_found"].format(len(filtered_df), st.session_state.page, total_pages-1))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  # Рендеринг только видимых карточек
134
  for idx in range(start_idx, end_idx):
135
  row = filtered_df.iloc[idx]
136
+ render_character_card(locale, row)
137
 
138
  # Кнопки навигации
139
  if total_pages > 1:
140
  cols = st.columns(4)
141
  with cols[0]:
142
+ if st.button(locale["pages_first"], disabled=(st.session_state.page == 0)):
143
  st.session_state.page = 0
144
  st.rerun()
145
  with cols[1]:
146
+ if st.button(locale["pages_before"], disabled=(st.session_state.page == 0)):
147
  st.session_state.page -= 1
148
  st.rerun()
149
  with cols[2]:
150
+ if st.button(locale["pages_next"], disabled=(st.session_state.page >= total_pages-1)):
151
  st.session_state.page += 1
152
  st.rerun()
153
  with cols[3]:
154
+ if st.button(locale["pages_last"], disabled=(st.session_state.page == total_pages-1)):
155
  st.session_state.page = total_pages-1
156
+ st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils.py CHANGED
@@ -12,14 +12,14 @@ def get_search_mask(df, search_query):
12
  )
13
 
14
  @st.cache_data
15
- def apply_filters(df, search_query, absurdity_range, selected_mood, selected_tags, sort_option):
16
  """Применение фильтров и сортировки"""
17
  filtered_df = df[
18
  (df["absurdity"] >= absurdity_range[0]) &
19
  (df["absurdity"] <= absurdity_range[1])
20
  ]
21
 
22
- if selected_mood != "Все":
23
  filtered_df = filtered_df[filtered_df["mood"] == selected_mood]
24
 
25
  if selected_tags:
@@ -31,10 +31,10 @@ def apply_filters(df, search_query, absurdity_range, selected_mood, selected_tag
31
 
32
  # Сортировка
33
  sort_columns = {
34
- "По имени (А→Я)": ("name", True),
35
- "По имени (Я→А)": ("name", False),
36
- "По абсурдности (↑)": ("absurdity", True),
37
- "По абсурдности (↓)": ("absurdity", False)
38
  }
39
  col, asc = sort_columns[sort_option]
40
  return filtered_df.sort_values(col, ascending=asc)
 
12
  )
13
 
14
  @st.cache_data
15
+ def apply_filters(locale, df, search_query, absurdity_range, selected_mood, selected_tags, sort_option):
16
  """Применение фильтров и сортировки"""
17
  filtered_df = df[
18
  (df["absurdity"] >= absurdity_range[0]) &
19
  (df["absurdity"] <= absurdity_range[1])
20
  ]
21
 
22
+ if selected_mood != locale["filter_all"]:
23
  filtered_df = filtered_df[filtered_df["mood"] == selected_mood]
24
 
25
  if selected_tags:
 
31
 
32
  # Сортировка
33
  sort_columns = {
34
+ locale["filter_sort"][0]: ("name", True),
35
+ locale["filter_sort"][1]: ("name", False),
36
+ locale["filter_sort"][2]: ("absurdity", True),
37
+ locale["filter_sort"][3]: ("absurdity", False)
38
  }
39
  col, asc = sort_columns[sort_option]
40
  return filtered_df.sort_values(col, ascending=asc)