sammoftah commited on
Commit
cd37f1f
·
verified ·
1 Parent(s): c80d59f

Deploy Dataset Tinder

Browse files
Files changed (6) hide show
  1. README.md +15 -7
  2. app.py +62 -0
  3. requirements.txt +1 -0
  4. shared/components.py +320 -0
  5. shared/styles.css +520 -0
  6. shared/utils.py +366 -0
README.md CHANGED
@@ -1,12 +1,20 @@
1
  ---
2
- title: Hf Master Dataset Tinder
3
- emoji: 🐢
4
- colorFrom: red
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.13.0
8
  app_file: app.py
9
- pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Dataset Tinder
3
+ emoji: ❤️
4
+ colorFrom: pink
5
+ colorTo: red
6
  sdk: gradio
 
7
  app_file: app.py
 
8
  ---
9
 
10
+ # Dataset Tinder
11
+
12
+ Swipe through HuggingFace datasets
13
+
14
+ ## Quick Start
15
+ ```bash
16
+ pip install -r requirements.txt
17
+ python app.py
18
+ ```
19
+
20
+ Part of [HF-Projects](https://github.com/OsamaMoftah/HF-projects)
app.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dataset Tinder - Swipe through HuggingFace datasets"""
2
+ import gradio as gr
3
+ import random
4
+ import sys, os
5
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
6
+ from shared.components import create_header, create_footer
7
+
8
+ SAMPLE_DATASETS = [
9
+ {"name": "squad", "description": "100k+ QA pairs", "size": "35MB", "likes": 1200},
10
+ {"name": "common_voice", "description": "Multilingual speech", "size": "2.3GB", "likes": 890},
11
+ {"name": "imagenet", "description": "14M images", "size": "150GB", "likes": 2100},
12
+ {"name": "wikitext", "description": "100M tokens", "size": "180MB", "likes": 750},
13
+ ]
14
+
15
+ def get_next_dataset(swipe_count):
16
+ dataset = random.choice(SAMPLE_DATASETS)
17
+ card = f"""
18
+ # 💾 {dataset['name']}
19
+ **{dataset['description']}**
20
+ - Size: {dataset['size']}
21
+ - ❤️ {dataset['likes']} likes
22
+ """
23
+ return card, swipe_count
24
+
25
+ def swipe_right(swipes):
26
+ return f"✅ Saved! {swipes + 1} datasets in your collection", swipes + 1
27
+
28
+ def swipe_left(swipes):
29
+ return "👈 Skipped", swipes
30
+
31
+ custom_css = """
32
+ .gradio-container {
33
+ font-family: 'Inter', sans-serif;
34
+ background:
35
+ radial-gradient(circle at top left, rgba(78, 205, 196, 0.12), transparent 28%),
36
+ radial-gradient(circle at top right, rgba(255, 107, 107, 0.10), transparent 30%);
37
+ }
38
+
39
+ .dataset-card {
40
+ background: rgba(255,255,255,0.05);
41
+ border: 1px solid rgba(255,255,255,0.10);
42
+ border-radius: 20px;
43
+ padding: 1rem 1.1rem;
44
+ box-shadow: 0 18px 36px rgba(0,0,0,0.14);
45
+ }
46
+ """
47
+
48
+ with gr.Blocks(css=custom_css, title="Dataset Tinder", theme=gr.themes.Soft()) as app:
49
+ create_header("Dataset Tinder", "Swipe through datasets • Build your collection", "❤️")
50
+ swipes_state = gr.State(0)
51
+ dataset_card = gr.Markdown()
52
+ with gr.Row():
53
+ left_btn = gr.Button("👎 Skip", size="lg")
54
+ right_btn = gr.Button("❤️ Save", variant="primary", size="lg")
55
+ status = gr.Markdown()
56
+ left_btn.click(swipe_left, swipes_state, status).then(get_next_dataset, swipes_state, dataset_card)
57
+ right_btn.click(swipe_right, swipes_state, [status, swipes_state]).then(get_next_dataset, swipes_state, dataset_card)
58
+ app.load(lambda: get_next_dataset(0), outputs=dataset_card)
59
+ create_footer("Dataset Tinder")
60
+
61
+ if __name__ == "__main__":
62
+ app.launch(server_name="0.0.0.0", server_port=7860)
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ gradio>=4.0.0
shared/components.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HF-Master Shared Components
3
+ Reusable Gradio components for all projects
4
+ """
5
+
6
+ import gradio as gr
7
+ from typing import List, Tuple, Optional, Dict, Any
8
+
9
+
10
+ class SharedComponents:
11
+ """Shared UI components for all HF-Master projects"""
12
+
13
+ @staticmethod
14
+ def create_header(title: str, description: str, emoji: str = "🚀") -> gr.Markdown:
15
+ """Create standardized project header"""
16
+ return gr.Markdown(f"""
17
+ <div style="
18
+ background: linear-gradient(135deg, rgba(78,205,196,0.14), rgba(255,107,107,0.16));
19
+ border: 1px solid rgba(255,255,255,0.12);
20
+ border-radius: 24px;
21
+ padding: 2rem 1.5rem;
22
+ margin-bottom: 1.25rem;
23
+ box-shadow: 0 18px 40px rgba(0,0,0,0.18);
24
+ backdrop-filter: blur(14px);
25
+ ">
26
+ <div style="display:flex; align-items:center; gap:0.75rem; justify-content:center; flex-wrap:wrap; margin-bottom:0.65rem;">
27
+ <span style="
28
+ display:inline-flex;
29
+ align-items:center;
30
+ justify-content:center;
31
+ width:3rem;
32
+ height:3rem;
33
+ border-radius:999px;
34
+ background: rgba(255,255,255,0.08);
35
+ border: 1px solid rgba(255,255,255,0.12);
36
+ font-size: 1.4rem;
37
+ ">{emoji}</span>
38
+ <h1 style="margin:0; font-size: clamp(2rem, 4vw, 3rem); letter-spacing:-0.03em;">{title}</h1>
39
+ </div>
40
+ <p style="margin:0; text-align:center; color:#cbd5e1; font-size:1.05rem; line-height:1.6;">{description}</p>
41
+ </div>
42
+ """)
43
+
44
+ @staticmethod
45
+ def create_footer(version: str = "1.0.0") -> gr.Markdown:
46
+ """Create standardized project footer"""
47
+ return gr.Markdown(f"""
48
+ <div style="
49
+ margin-top: 1.5rem;
50
+ padding: 1rem 1.25rem;
51
+ border-top: 1px solid rgba(255,255,255,0.10);
52
+ color: #94a3b8;
53
+ text-align: center;
54
+ font-size: 0.95rem;
55
+ ">
56
+ <strong style="color:#e2e8f0;">HF-Master v{version}</strong> | Built with ❤️ for the AI community
57
+ </div>
58
+ """)
59
+
60
+ @staticmethod
61
+ def create_status_badge(status: str) -> str:
62
+ """Create status badge"""
63
+ colors = {
64
+ "complete": "🟢",
65
+ "in-progress": "🟡",
66
+ "planned": "⚪",
67
+ "experimental": "🔴"
68
+ }
69
+ return colors.get(status.lower(), "⚪")
70
+
71
+ @staticmethod
72
+ def create_project_card(
73
+ title: str,
74
+ description: str,
75
+ tech_stack: List[str],
76
+ difficulty: str,
77
+ viral_potential: str
78
+ ) -> str:
79
+ """Create markdown project card"""
80
+ tech_badges = " ".join([f"`{t}`" for t in tech_stack])
81
+
82
+ return f"""
83
+ ## {title}
84
+
85
+ {description}
86
+
87
+ **Tech Stack:** {tech_badges}
88
+
89
+ **Difficulty:** {difficulty} | **Viral Potential:** {viral_potential}
90
+ """
91
+
92
+ @staticmethod
93
+ def create_risk_chart(risk_factors: Dict[str, float]) -> Any:
94
+ """Create risk factor visualization"""
95
+ import plotly.graph_objects as go
96
+
97
+ factors = list(risk_factors.keys())
98
+ scores = [risk_factors[f] * 100 for f in factors]
99
+
100
+ fig = go.Figure(data=[
101
+ go.Bar(
102
+ x=scores,
103
+ y=[f.replace('_', ' ').title() for f in factors],
104
+ orientation='h',
105
+ marker=dict(
106
+ color=scores,
107
+ colorscale='RdYlGn_r',
108
+ cmin=0,
109
+ cmax=100
110
+ )
111
+ )
112
+ ])
113
+
114
+ fig.update_layout(
115
+ title="Risk Factor Breakdown",
116
+ xaxis_title="Risk Score",
117
+ yaxis_title="Factor",
118
+ height=400,
119
+ template="plotly_white"
120
+ )
121
+
122
+ return fig
123
+
124
+ @staticmethod
125
+ def create_comparison_chart(items: List[Dict], keys: List[str]) -> Any:
126
+ """Create comparison visualization"""
127
+ import plotly.graph_objects as go
128
+
129
+ fig = go.Figure()
130
+
131
+ for i, item in enumerate(items):
132
+ fig.add_trace(go.Bar(
133
+ name=item.get('name', f'Item {i+1}'),
134
+ x=keys,
135
+ y=[item.get(k, 0) for k in keys]
136
+ ))
137
+
138
+ fig.update_layout(
139
+ barmode='group',
140
+ height=400
141
+ )
142
+
143
+ return fig
144
+
145
+ @staticmethod
146
+ def create_metric_card(label: str, value: str, emoji: str = "📊") -> gr.Markdown:
147
+ """Create metric display card"""
148
+ return gr.Markdown(f"""
149
+ ### {emoji} {label}
150
+
151
+ **{value}**
152
+ """)
153
+
154
+ @staticmethod
155
+ def create_error_display(error: str) -> gr.Markdown:
156
+ """Create error message display"""
157
+ return gr.Markdown(f"""
158
+ ❌ **Error**
159
+
160
+ {error}
161
+ """)
162
+
163
+ @staticmethod
164
+ def create_success_display(message: str) -> gr.Markdown:
165
+ """Create success message display"""
166
+ return gr.Markdown(f"""
167
+ ✅ **Success**
168
+
169
+ {message}
170
+ """)
171
+
172
+
173
+ class LoadingSpinner:
174
+ """Loading state display"""
175
+
176
+ @staticmethod
177
+ def create_spinner(message: str = "Loading...") -> gr.Markdown:
178
+ """Create loading spinner"""
179
+ return gr.Markdown(f"""
180
+ ⏳ **{message}**
181
+
182
+ _This may take a moment..._
183
+ """)
184
+
185
+ @staticmethod
186
+ def create_progress_bar(initial: float = 0) -> gr.Markdown:
187
+ """Create progress display"""
188
+ return gr.Markdown(f"""
189
+ ░░░░░░░░░ **{initial}%**
190
+ """)
191
+
192
+
193
+ class TableFormatter:
194
+ """Format data as tables"""
195
+
196
+ @staticmethod
197
+ def format_dict_table(data: Dict[str, Any], headers: List[str] = None) -> List:
198
+ """Format dictionary as table rows"""
199
+ if not headers:
200
+ headers = ["Key", "Value"]
201
+
202
+ rows = []
203
+ for key, value in data.items():
204
+ rows.append([key, str(value)])
205
+
206
+ return [headers] + rows
207
+
208
+ @staticmethod
209
+ def create_dataframe(data: List[Dict], columns: List[str] = None) -> List:
210
+ """Create dataframe-compatible data structure"""
211
+ if not data:
212
+ return []
213
+
214
+ if columns:
215
+ headers = columns
216
+ else:
217
+ headers = list(data[0].keys()) if data else []
218
+
219
+ rows = [[row.get(h, "") for h in headers] for row in data]
220
+
221
+ return [headers] + rows
222
+
223
+
224
+ class CodeHighlighter:
225
+ """Code display and highlighting"""
226
+
227
+ @staticmethod
228
+ def create_code_display(code: str, language: str = "python") -> gr.Code:
229
+ """Create code display block"""
230
+ return gr.Code(
231
+ value=code,
232
+ language=language,
233
+ lines=20
234
+ )
235
+
236
+ @staticmethod
237
+ def create_copy_button(code: str) -> gr.Button:
238
+ """Create copy-to-clipboard button"""
239
+ return gr.Button("📋 Copy Code")
240
+
241
+ @staticmethod
242
+ def create_diff_view(old_code: str, new_code: str) -> Tuple[gr.Code, gr.Code]:
243
+ """Create side-by-side diff view"""
244
+ return (
245
+ gr.Code(value=old_code, language="python", lines=15, label="Before"),
246
+ gr.Code(value=new_code, language="python", lines=15, label="After")
247
+ )
248
+
249
+
250
+ def create_header(title: str, description: str, emoji: str = "🚀") -> gr.Markdown:
251
+ return SharedComponents.create_header(title, description, emoji)
252
+
253
+
254
+ def create_footer(version: str = "1.0.0") -> gr.Markdown:
255
+ return SharedComponents.create_footer(version)
256
+
257
+
258
+ class ProgressTracker:
259
+ """Track multi-step progress"""
260
+
261
+ def __init__(self, steps: List[str]):
262
+ self.steps = steps
263
+ self.current = 0
264
+
265
+ def get_status(self) -> str:
266
+ """Get current status"""
267
+ completed = "✅ " + "\n".join(self.steps[:self.current])
268
+ current = f"🔄 {self.steps[self.current]}" if self.current < len(self.steps) else ""
269
+ remaining = "\n".join([f"⬜ {s}" for s in self.steps[self.current+1:]])
270
+
271
+ return f"""
272
+ ## Progress
273
+
274
+ {completed}
275
+ {current}
276
+ {remaining}
277
+ """
278
+
279
+ def advance(self) -> bool:
280
+ """Move to next step"""
281
+ if self.current < len(self.steps):
282
+ self.current += 1
283
+ return True
284
+ return False
285
+
286
+ def reset(self):
287
+ """Reset progress"""
288
+ self.current = 0
289
+
290
+
291
+ def create_tabbed_interface(tabs: Dict[str, Any]) -> gr.Blocks:
292
+ """Create tabbed interface helper"""
293
+ with gr.Blocks() as demo:
294
+ with gr.Tabs():
295
+ for tab_name, tab_content in tabs.items():
296
+ with gr.Tab(tab_name):
297
+ tab_content
298
+
299
+ return demo
300
+
301
+
302
+ def create_side_by_side(left_content: Any, right_content: Any) -> Tuple[gr.Column, gr.Column]:
303
+ """Create side-by-side layout"""
304
+ with gr.Row():
305
+ with gr.Column():
306
+ left_content
307
+ with gr.Column():
308
+ right_content
309
+
310
+ return left_content, right_content
311
+
312
+
313
+ def create_accordion(items: List[Tuple[str, Any]]) -> gr.Accordion:
314
+ """Create accordion-style expandable sections"""
315
+ with gr.Accordion("Click to expand") as accordion:
316
+ for title, content in items:
317
+ gr.Markdown(f"### {title}")
318
+ content
319
+
320
+ return accordion
shared/styles.css ADDED
@@ -0,0 +1,520 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* HF-Master Shared Styles */
2
+
3
+ /* CSS Variables for theming */
4
+ :root {
5
+ --primary-color: #ff6b6b;
6
+ --secondary-color: #4ecdc4;
7
+ --accent-color: #ffe66d;
8
+ --background-dark: #0f172a;
9
+ --background-light: #111827;
10
+ --surface-elevated: rgba(17, 24, 39, 0.82);
11
+ --surface-soft: rgba(255, 255, 255, 0.05);
12
+ --text-primary: #f8fafc;
13
+ --text-secondary: #cbd5e1;
14
+ --success-color: #22c55e;
15
+ --warning-color: #f59e0b;
16
+ --error-color: #ef4444;
17
+ --border-radius: 16px;
18
+ --box-shadow: 0 20px 45px rgba(0, 0, 0, 0.28);
19
+ --border-color: rgba(255, 255, 255, 0.12);
20
+ }
21
+
22
+ /* Global Styles */
23
+ html {
24
+ scroll-behavior: smooth;
25
+ }
26
+
27
+ body {
28
+ font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
29
+ background:
30
+ radial-gradient(circle at top left, rgba(78, 205, 196, 0.18), transparent 30%),
31
+ radial-gradient(circle at top right, rgba(255, 107, 107, 0.16), transparent 32%),
32
+ linear-gradient(180deg, #0f172a 0%, #111827 100%);
33
+ color: var(--text-primary);
34
+ }
35
+
36
+ .main {
37
+ background: transparent;
38
+ }
39
+
40
+ /* Streamlit shell polish */
41
+ .block-container {
42
+ padding-top: 2rem;
43
+ padding-bottom: 2rem;
44
+ max-width: 1200px;
45
+ }
46
+
47
+ div[data-testid="stHeader"] {
48
+ background: transparent;
49
+ }
50
+
51
+ div[data-testid="stSidebar"] {
52
+ background: linear-gradient(180deg, rgba(15, 23, 42, 0.96) 0%, rgba(17, 24, 39, 0.96) 100%);
53
+ border-right: 1px solid var(--border-color);
54
+ }
55
+
56
+ div[data-testid="stToolbar"] {
57
+ background: transparent;
58
+ }
59
+
60
+ /* Hero / surface helpers */
61
+ .hero {
62
+ background:
63
+ linear-gradient(135deg, rgba(78, 205, 196, 0.12), rgba(255, 107, 107, 0.14)),
64
+ rgba(255, 255, 255, 0.04);
65
+ border: 1px solid var(--border-color);
66
+ border-radius: 24px;
67
+ padding: 2rem 1.5rem;
68
+ margin: 0 0 1.5rem 0;
69
+ box-shadow: var(--box-shadow);
70
+ text-align: center;
71
+ backdrop-filter: blur(14px);
72
+ }
73
+
74
+ .hero h1,
75
+ .hero h2,
76
+ .hero p {
77
+ margin: 0;
78
+ }
79
+
80
+ .hero h1 {
81
+ font-size: clamp(2rem, 4vw, 3.25rem);
82
+ letter-spacing: -0.03em;
83
+ margin-bottom: 0.4rem;
84
+ }
85
+
86
+ .hero p {
87
+ color: var(--text-secondary);
88
+ font-size: 1.05rem;
89
+ line-height: 1.6;
90
+ }
91
+
92
+ .glass-card,
93
+ .project-card,
94
+ .info-card,
95
+ .metric-card {
96
+ background: var(--surface-elevated);
97
+ border: 1px solid var(--border-color);
98
+ border-radius: var(--border-radius);
99
+ box-shadow: var(--box-shadow);
100
+ backdrop-filter: blur(14px);
101
+ }
102
+
103
+ /* Card Styles */
104
+ .project-card {
105
+ padding: 1.5rem;
106
+ margin: 1rem 0;
107
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
108
+ }
109
+
110
+ .project-card:hover {
111
+ transform: translateY(-2px);
112
+ box-shadow: 0 28px 52px rgba(0, 0, 0, 0.35);
113
+ }
114
+
115
+ /* Badge Styles */
116
+ .badge {
117
+ display: inline-block;
118
+ padding: 0.25rem 0.75rem;
119
+ border-radius: 9999px;
120
+ font-size: 0.75rem;
121
+ font-weight: 600;
122
+ text-transform: uppercase;
123
+ letter-spacing: 0.05em;
124
+ }
125
+
126
+ .badge-success {
127
+ background-color: rgba(46, 204, 113, 0.2);
128
+ color: var(--success-color);
129
+ border: 1px solid var(--success-color);
130
+ }
131
+
132
+ .badge-warning {
133
+ background-color: rgba(243, 156, 18, 0.2);
134
+ color: var(--warning-color);
135
+ border: 1px solid var(--warning-color);
136
+ }
137
+
138
+ .badge-error {
139
+ background-color: rgba(231, 76, 60, 0.2);
140
+ color: var(--error-color);
141
+ border: 1px solid var(--error-color);
142
+ }
143
+
144
+ .badge-info {
145
+ background-color: rgba(78, 205, 196, 0.2);
146
+ color: var(--secondary-color);
147
+ border: 1px solid var(--secondary-color);
148
+ }
149
+
150
+ /* Tech Stack Tags */
151
+ .tech-tag {
152
+ display: inline-block;
153
+ background-color: rgba(255, 107, 107, 0.15);
154
+ color: var(--primary-color);
155
+ padding: 0.25rem 0.5rem;
156
+ border-radius: 6px;
157
+ font-size: 0.75rem;
158
+ font-family: 'Monaco', 'Menlo', monospace;
159
+ margin: 0.125rem;
160
+ }
161
+
162
+ /* Markdown Content Styles */
163
+ .markdown-content {
164
+ line-height: 1.7;
165
+ }
166
+
167
+ .markdown-content h1 {
168
+ color: var(--primary-color);
169
+ border-bottom: 2px solid var(--primary-color);
170
+ padding-bottom: 0.5rem;
171
+ margin-top: 1.5rem;
172
+ }
173
+
174
+ .markdown-content h2 {
175
+ color: var(--secondary-color);
176
+ margin-top: 1.25rem;
177
+ }
178
+
179
+ .markdown-content h3 {
180
+ color: var(--accent-color);
181
+ }
182
+
183
+ .markdown-content code {
184
+ background-color: rgba(0, 0, 0, 0.35);
185
+ padding: 0.125rem 0.375rem;
186
+ border-radius: 4px;
187
+ font-family: 'Monaco', 'Menlo', monospace;
188
+ font-size: 0.875em;
189
+ }
190
+
191
+ .markdown-content pre {
192
+ background-color: rgba(2, 6, 23, 0.8);
193
+ padding: 1rem;
194
+ border-radius: 8px;
195
+ overflow-x: auto;
196
+ border: 1px solid var(--border-color);
197
+ }
198
+
199
+ .markdown-content pre code {
200
+ background: none;
201
+ padding: 0;
202
+ }
203
+
204
+ /* Table Styles */
205
+ .data-table {
206
+ width: 100%;
207
+ border-collapse: collapse;
208
+ margin: 1rem 0;
209
+ }
210
+
211
+ .data-table th {
212
+ background-color: var(--primary-color);
213
+ color: white;
214
+ padding: 0.75rem;
215
+ text-align: left;
216
+ font-weight: 600;
217
+ }
218
+
219
+ .data-table td {
220
+ padding: 0.75rem;
221
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
222
+ }
223
+
224
+ .data-table tr:hover {
225
+ background-color: rgba(255, 255, 255, 0.05);
226
+ }
227
+
228
+ /* Animation Classes */
229
+ @keyframes fadeIn {
230
+ from {
231
+ opacity: 0;
232
+ transform: translateY(10px);
233
+ }
234
+ to {
235
+ opacity: 1;
236
+ transform: translateY(0);
237
+ }
238
+ }
239
+
240
+ .fade-in {
241
+ animation: fadeIn 0.3s ease-out;
242
+ }
243
+
244
+ @keyframes pulse {
245
+ 0%, 100% {
246
+ opacity: 1;
247
+ }
248
+ 50% {
249
+ opacity: 0.5;
250
+ }
251
+ }
252
+
253
+ .pulse {
254
+ animation: pulse 2s ease-in-out infinite;
255
+ }
256
+
257
+ /* Loading Spinner */
258
+ .spinner {
259
+ display: inline-block;
260
+ width: 2rem;
261
+ height: 2rem;
262
+ border: 3px solid rgba(255, 255, 255, 0.1);
263
+ border-top-color: var(--primary-color);
264
+ border-radius: 50%;
265
+ animation: spin 0.8s linear infinite;
266
+ }
267
+
268
+ @keyframes spin {
269
+ to {
270
+ transform: rotate(360deg);
271
+ }
272
+ }
273
+
274
+ /* Streamlit input polish */
275
+ div[data-baseweb="input"],
276
+ div[data-baseweb="textarea"],
277
+ div[data-baseweb="select"] {
278
+ background: rgba(255, 255, 255, 0.03);
279
+ }
280
+
281
+ textarea,
282
+ input,
283
+ select {
284
+ color: var(--text-primary) !important;
285
+ }
286
+
287
+ div[data-testid="stTextInput"] input,
288
+ div[data-testid="stTextArea"] textarea,
289
+ div[data-testid="stSelectbox"] div {
290
+ border-radius: 14px !important;
291
+ border-color: var(--border-color) !important;
292
+ }
293
+
294
+ div[data-testid="stButton"] > button {
295
+ border-radius: 999px;
296
+ border: 1px solid rgba(255, 255, 255, 0.16);
297
+ background: linear-gradient(135deg, var(--primary-color), #fb7185);
298
+ color: white;
299
+ font-weight: 700;
300
+ padding: 0.7rem 1.15rem;
301
+ box-shadow: 0 14px 30px rgba(255, 107, 107, 0.22);
302
+ transition: transform 0.18s ease, box-shadow 0.18s ease;
303
+ }
304
+
305
+ div[data-testid="stButton"] > button:hover {
306
+ transform: translateY(-1px);
307
+ box-shadow: 0 18px 34px rgba(255, 107, 107, 0.28);
308
+ }
309
+
310
+ div[data-testid="stMetric"] {
311
+ background: rgba(255, 255, 255, 0.04);
312
+ border: 1px solid var(--border-color);
313
+ border-radius: 18px;
314
+ padding: 0.8rem 1rem;
315
+ box-shadow: var(--box-shadow);
316
+ }
317
+
318
+ /* Small utility wrappers for markdown HTML blocks */
319
+ .pill-row {
320
+ display: flex;
321
+ flex-wrap: wrap;
322
+ gap: 0.5rem;
323
+ justify-content: center;
324
+ }
325
+
326
+ .pill {
327
+ display: inline-flex;
328
+ align-items: center;
329
+ gap: 0.35rem;
330
+ padding: 0.45rem 0.8rem;
331
+ border-radius: 999px;
332
+ background: rgba(255, 255, 255, 0.07);
333
+ border: 1px solid var(--border-color);
334
+ color: var(--text-primary);
335
+ font-size: 0.85rem;
336
+ }
337
+
338
+ /* Metric Cards */
339
+ .metric-card {
340
+ background: linear-gradient(135deg, var(--background-light) 0%, rgba(78, 205, 196, 0.1) 100%);
341
+ border-radius: var(--border-radius);
342
+ padding: 1.5rem;
343
+ text-align: center;
344
+ border: 1px solid rgba(78, 205, 196, 0.2);
345
+ }
346
+
347
+ .metric-card .value {
348
+ font-size: 2.5rem;
349
+ font-weight: 700;
350
+ color: var(--secondary-color);
351
+ }
352
+
353
+ .metric-card .label {
354
+ font-size: 0.875rem;
355
+ color: var(--text-secondary);
356
+ text-transform: uppercase;
357
+ letter-spacing: 0.1em;
358
+ }
359
+
360
+ /* Gradio Overrides */
361
+ .gradio-container {
362
+ max-width: 1200px !important;
363
+ margin: 0 auto !important;
364
+ }
365
+
366
+ /* Tab Styles */
367
+ .tab-nav button {
368
+ background-color: transparent !important;
369
+ border: none !important;
370
+ border-bottom: 3px solid transparent !important;
371
+ transition: all 0.2s ease;
372
+ }
373
+
374
+ .tab-nav button:hover {
375
+ background-color: rgba(255, 255, 255, 0.05) !important;
376
+ }
377
+
378
+ .tab-nav button.selected {
379
+ border-bottom-color: var(--primary-color) !important;
380
+ color: var(--primary-color) !important;
381
+ }
382
+
383
+ /* Footer Styles */
384
+ .footer {
385
+ text-align: center;
386
+ padding: 2rem 0;
387
+ margin-top: 3rem;
388
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
389
+ color: var(--text-secondary);
390
+ font-size: 0.875rem;
391
+ }
392
+
393
+ /* Status Indicators */
394
+ .status-indicator {
395
+ display: inline-flex;
396
+ align-items: center;
397
+ gap: 0.5rem;
398
+ }
399
+
400
+ .status-dot {
401
+ width: 8px;
402
+ height: 8px;
403
+ border-radius: 50%;
404
+ }
405
+
406
+ .status-dot.complete {
407
+ background-color: var(--success-color);
408
+ }
409
+
410
+ .status-dot.in-progress {
411
+ background-color: var(--warning-color);
412
+ animation: pulse 2s infinite;
413
+ }
414
+
415
+ .status-dot.planned {
416
+ background-color: var(--text-secondary);
417
+ }
418
+
419
+ /* Difficulty Badges */
420
+ .difficulty {
421
+ padding: 0.25rem 0.75rem;
422
+ border-radius: 4px;
423
+ font-size: 0.75rem;
424
+ font-weight: 600;
425
+ }
426
+
427
+ .difficulty.beginner {
428
+ background-color: rgba(46, 204, 113, 0.2);
429
+ color: #2ECC71;
430
+ }
431
+
432
+ .difficulty.intermediate {
433
+ background-color: rgba(243, 156, 18, 0.2);
434
+ color: #F39C12;
435
+ }
436
+
437
+ .difficulty.advanced {
438
+ background-color: rgba(231, 76, 60, 0.2);
439
+ color: #E74C3C;
440
+ }
441
+
442
+ .difficulty.expert {
443
+ background-color: rgba(155, 89, 182, 0.2);
444
+ color: #9B59B6;
445
+ }
446
+
447
+ /* Responsive Grid */
448
+ .grid-2 {
449
+ display: grid;
450
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
451
+ gap: 1.5rem;
452
+ }
453
+
454
+ .grid-3 {
455
+ display: grid;
456
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
457
+ gap: 1.5rem;
458
+ }
459
+
460
+ /* Custom Scrollbar */
461
+ ::-webkit-scrollbar {
462
+ width: 8px;
463
+ height: 8px;
464
+ }
465
+
466
+ ::-webkit-scrollbar-track {
467
+ background: var(--background-dark);
468
+ }
469
+
470
+ ::-webkit-scrollbar-thumb {
471
+ background: var(--text-secondary);
472
+ border-radius: 4px;
473
+ }
474
+
475
+ ::-webkit-scrollbar-thumb:hover {
476
+ background: var(--primary-color);
477
+ }
478
+
479
+ /* Utility Classes */
480
+ .text-center {
481
+ text-align: center;
482
+ }
483
+
484
+ .text-muted {
485
+ color: var(--text-secondary);
486
+ }
487
+
488
+ .mt-1 { margin-top: 0.5rem; }
489
+ .mt-2 { margin-top: 1rem; }
490
+ .mt-3 { margin-top: 1.5rem; }
491
+ .mt-4 { margin-top: 2rem; }
492
+
493
+ .mb-1 { margin-bottom: 0.5rem; }
494
+ .mb-2 { margin-bottom: 1rem; }
495
+ .mb-3 { margin-bottom: 1.5rem; }
496
+ .mb-4 { margin-bottom: 2rem; }
497
+
498
+ .p-1 { padding: 0.5rem; }
499
+ .p-2 { padding: 1rem; }
500
+ .p-3 { padding: 1.5rem; }
501
+ .p-4 { padding: 2rem; }
502
+
503
+ .flex {
504
+ display: flex;
505
+ }
506
+
507
+ .flex-center {
508
+ display: flex;
509
+ align-items: center;
510
+ justify-content: center;
511
+ }
512
+
513
+ .gap-1 { gap: 0.5rem; }
514
+ .gap-2 { gap: 1rem; }
515
+ .gap-3 { gap: 1.5rem; }
516
+
517
+ /* Hidden utility */
518
+ .hidden {
519
+ display: none !important;
520
+ }
shared/utils.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HF-Master Shared Utilities
3
+ Helper functions for all projects
4
+ """
5
+
6
+ import os
7
+ import re
8
+ import json
9
+ import hashlib
10
+ from typing import Dict, List, Optional, Any, Union
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+ import sqlite3
14
+
15
+
16
+ def load_env(var_name: str, default: Optional[str] = None) -> Optional[str]:
17
+ """Load environment variable with optional default"""
18
+ return os.getenv(var_name, default)
19
+
20
+
21
+ def load_api_key(provider: str = "openai") -> Optional[str]:
22
+ """Load API key for specified provider"""
23
+ key_map = {
24
+ "openai": "OPENAI_API_KEY",
25
+ "anthropic": "ANTHROPIC_API_KEY",
26
+ "huggingface": "HF_TOKEN",
27
+ "cohere": "COHERE_API_KEY",
28
+ "together": "TOGETHER_API_KEY"
29
+ }
30
+
31
+ env_var = key_map.get(provider.lower())
32
+ if env_var:
33
+ return load_env(env_var)
34
+
35
+ return None
36
+
37
+
38
+ def estimate_token_count(text: str, model: str = "gpt-4") -> int:
39
+ """Estimate token count for text"""
40
+ tokens_per_word = {
41
+ "gpt-4": 4, # ~4 chars per token
42
+ "gpt-3.5": 4,
43
+ "claude": 4,
44
+ "llama": 3 # More efficient
45
+ }
46
+
47
+ chars_per_token = tokens_per_word.get(model, 4)
48
+ return len(text) // chars_per_token
49
+
50
+
51
+ def estimate_tokens(text: str, model: str = "gpt-4") -> int:
52
+ """Backward-compatible alias used by older apps"""
53
+ return estimate_token_count(text, model)
54
+
55
+
56
+ def calculate_api_cost(
57
+ model: str,
58
+ input_tokens: int,
59
+ output_tokens: int,
60
+ provider: str = "openai"
61
+ ) -> float:
62
+ """Calculate API cost for model usage"""
63
+
64
+ pricing = {
65
+ "openai": {
66
+ "gpt-4": {"input": 0.03, "output": 0.06},
67
+ "gpt-3.5-turbo": {"input": 0.001, "output": 0.002},
68
+ "gpt-4-turbo": {"input": 0.01, "output": 0.03}
69
+ },
70
+ "anthropic": {
71
+ "claude-3-opus": {"input": 0.015, "output": 0.075},
72
+ "claude-3-sonnet": {"input": 0.003, "output": 0.015}
73
+ }
74
+ }
75
+
76
+ provider_pricing = pricing.get(provider, {})
77
+ model_pricing = provider_pricing.get(model, {"input": 0.01, "output": 0.03})
78
+
79
+ input_cost = (input_tokens / 1000) * model_pricing["input"]
80
+ output_cost = (output_tokens / 1000) * model_pricing["output"]
81
+
82
+ return input_cost + output_cost
83
+
84
+
85
+ def calculate_cost(tokens: int, model: str = "gpt-4", provider: str = "openai") -> float:
86
+ """Backward-compatible alias used by older apps"""
87
+ return calculate_api_cost(model=model, input_tokens=tokens, output_tokens=0, provider=provider)
88
+
89
+
90
+ def sanitize_filename(name: str) -> str:
91
+ """Convert string to safe filename"""
92
+ name = name.lower().strip()
93
+ name = re.sub(r'[^\w\s-]', '', name)
94
+ name = re.sub(r'[\s]+', '-', name)
95
+ return name
96
+
97
+
98
+ def create_hash(text: str, length: int = 8) -> str:
99
+ """Create short hash from text"""
100
+ return hashlib.md5(text.encode()).hexdigest()[:length]
101
+
102
+
103
+ def format_duration(seconds: float) -> str:
104
+ """Format duration in human-readable form"""
105
+ if seconds < 60:
106
+ return f"{seconds:.1f}s"
107
+ elif seconds < 3600:
108
+ return f"{seconds/60:.1f}m"
109
+ else:
110
+ return f"{seconds/3600:.1f}h"
111
+
112
+
113
+ def format_bytes(bytes: int) -> str:
114
+ """Format bytes in human-readable form"""
115
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
116
+ if bytes < 1024:
117
+ return f"{bytes:.1f} {unit}"
118
+ bytes /= 1024
119
+ return f"{bytes:.1f} PB"
120
+
121
+
122
+ def truncate_text(text: str, max_length: int = 100, suffix: str = "...") -> str:
123
+ """Truncate text with suffix"""
124
+ if len(text) <= max_length:
125
+ return text
126
+ return text[:max_length - len(suffix)] + suffix
127
+
128
+
129
+ def parse_dice_notation(notation: str) -> Dict[str, Any]:
130
+ """Parse dice notation like 2d6+3"""
131
+ match = re.match(r'(\d+)d(\d+)(kh\d+)?([+-]\d+)?', notation.upper())
132
+ if not match:
133
+ raise ValueError(f"Invalid dice notation: {notation}")
134
+
135
+ num_dice = int(match.group(1))
136
+ die_size = int(match.group(2))
137
+ keep_high = match.group(3)
138
+ modifier = int(match.group(4)) if match.group(4) else 0
139
+
140
+ return {
141
+ "num_dice": num_dice,
142
+ "die_size": die_size,
143
+ "keep_high": keep_high,
144
+ "modifier": modifier
145
+ }
146
+
147
+
148
+ def roll_dice(notation: str) -> List[int]:
149
+ """Roll dice and return individual rolls"""
150
+ import random
151
+
152
+ parsed = parse_dice_notation(notation)
153
+ rolls = [random.randint(1, parsed["die_size"]) for _ in range(parsed["num_dice"])]
154
+
155
+ if parsed["keep_high"]:
156
+ keep = int(parsed["keep_high"][2:])
157
+ rolls = sorted(rolls, reverse=True)[:keep]
158
+
159
+ return rolls
160
+
161
+
162
+ def calculate_modifier(ability_score: int) -> int:
163
+ """Calculate D&D ability modifier from score"""
164
+ return (ability_score - 10) // 2
165
+
166
+
167
+ def validate_ethereum_address(address: str) -> bool:
168
+ """Validate Ethereum address format"""
169
+ pattern = r'^0x[a-fA-F0-9]{40}$'
170
+ return bool(re.match(pattern, address))
171
+
172
+
173
+ def validate_solana_address(address: str) -> bool:
174
+ """Validate Solana address format"""
175
+ pattern = r'^[1-9A-HJ-NP-Za-km-z]{32,44}$'
176
+ return bool(re.match(pattern, address))
177
+
178
+
179
+ def extract_urls(text: str) -> List[str]:
180
+ """Extract URLs from text"""
181
+ url_pattern = r'https?://[^\s<>"{}|\\^`\[\]]+'
182
+ return re.findall(url_pattern, text)
183
+
184
+
185
+ def extract_code_blocks(text: str) -> List[str]:
186
+ """Extract code blocks from markdown text"""
187
+ pattern = r'```(?:\w+)?\n(.*?)```'
188
+ return re.findall(pattern, text, re.DOTALL)
189
+
190
+
191
+ def parse_math_expression(expr: str) -> float:
192
+ """Safely evaluate simple math expressions"""
193
+ allowed_chars = set("0123456789+-*/.() ")
194
+ if all(c in allowed_chars for c in expr):
195
+ return eval(expr)
196
+ raise ValueError(f"Unsafe expression: {expr}")
197
+
198
+
199
+ def create_timer(func):
200
+ """Decorator to time function execution"""
201
+ import time
202
+ from functools import wraps
203
+
204
+ @wraps(func)
205
+ def wrapper(*args, **kwargs):
206
+ start = time.time()
207
+ result = func(*args, **kwargs)
208
+ duration = time.time() - start
209
+ print(f"{func.__name__} took {format_duration(duration)}")
210
+ return result
211
+
212
+ return wrapper
213
+
214
+
215
+ def retry_on_failure(max_attempts: int = 3, delay: float = 1.0):
216
+ """Decorator to retry function on failure"""
217
+ from functools import wraps
218
+ import time
219
+
220
+ def decorator(func):
221
+ @wraps(func)
222
+ def wrapper(*args, **kwargs):
223
+ for attempt in range(max_attempts):
224
+ try:
225
+ return func(*args, **kwargs)
226
+ except Exception as e:
227
+ if attempt == max_attempts - 1:
228
+ raise
229
+ time.sleep(delay * (attempt + 1))
230
+
231
+ return wrapper
232
+
233
+ return decorator
234
+
235
+
236
+ class SimpleCache:
237
+ """Simple in-memory cache"""
238
+
239
+ def __init__(self, max_size: int = 100):
240
+ self.cache: Dict[str, Any] = {}
241
+ self.max_size = max_size
242
+ self.access_times: Dict[str, datetime] = {}
243
+
244
+ def get(self, key: str) -> Optional[Any]:
245
+ """Get value from cache"""
246
+ if key in self.cache:
247
+ self.access_times[key] = datetime.now()
248
+ return self.cache[key]
249
+ return None
250
+
251
+ def set(self, key: str, value: Any):
252
+ """Set value in cache"""
253
+ if len(self.cache) >= self.max_size:
254
+ oldest = min(self.access_times.items(), key=lambda x: x[1])[0]
255
+ del self.cache[oldest]
256
+ del self.access_times[oldest]
257
+
258
+ self.cache[key] = value
259
+ self.access_times[key] = datetime.now()
260
+
261
+ def clear(self):
262
+ """Clear cache"""
263
+ self.cache.clear()
264
+ self.access_times.clear()
265
+
266
+
267
+ class Database:
268
+ """Simple SQLite wrapper"""
269
+
270
+ def __init__(self, db_path: str = "data.db"):
271
+ self.db_path = db_path
272
+ Path(db_path).parent.mkdir(parents=True, exist_ok=True)
273
+ self.conn = None
274
+
275
+ def connect(self):
276
+ """Connect to database"""
277
+ self.conn = sqlite3.connect(self.db_path)
278
+ self.conn.row_factory = sqlite3.Row
279
+
280
+ def close(self):
281
+ """Close database connection"""
282
+ if self.conn:
283
+ self.conn.close()
284
+
285
+ def execute(self, query: str, params: tuple = ()) -> sqlite3.Cursor:
286
+ """Execute query"""
287
+ if not self.conn:
288
+ self.connect()
289
+ return self.conn.execute(query, params)
290
+
291
+ def commit(self):
292
+ """Commit transaction"""
293
+ if self.conn:
294
+ self.conn.commit()
295
+
296
+ def fetchall(self, query: str, params: tuple = ()) -> List[Dict]:
297
+ """Fetch all results"""
298
+ cursor = self.execute(query, params)
299
+ return [dict(row) for row in cursor.fetchall()]
300
+
301
+ def fetchone(self, query: str, params: tuple = ()) -> Optional[Dict]:
302
+ """Fetch one result"""
303
+ cursor = self.execute(query, params)
304
+ row = cursor.fetchone()
305
+ return dict(row) if row else None
306
+
307
+ def create_table(self, name: str, columns: Dict[str, str]):
308
+ """Create table with columns"""
309
+ cols = ", ".join([f"{k} {v}" for k, v in columns.items()])
310
+ self.execute(f"CREATE TABLE IF NOT EXISTS {name} ({cols})")
311
+ self.commit()
312
+
313
+
314
+ def load_json_file(filepath: str) -> Dict:
315
+ """Load JSON file"""
316
+ with open(filepath, 'r') as f:
317
+ return json.load(f)
318
+
319
+
320
+ def save_json_file(data: Dict, filepath: str):
321
+ """Save JSON file"""
322
+ Path(filepath).parent.mkdir(parents=True, exist_ok=True)
323
+ with open(filepath, 'w') as f:
324
+ json.dump(data, f, indent=2)
325
+
326
+
327
+ def merge_dicts(*dicts: Dict) -> Dict:
328
+ """Merge multiple dictionaries"""
329
+ result = {}
330
+ for d in dicts:
331
+ result.update(d)
332
+ return result
333
+
334
+
335
+ def flatten_list(nested: List[Any]) -> List[Any]:
336
+ """Flatten nested list"""
337
+ result = []
338
+ for item in nested:
339
+ if isinstance(item, list):
340
+ result.extend(flatten_list(item))
341
+ else:
342
+ result.append(item)
343
+ return result
344
+
345
+
346
+ def chunk_text(text: str, chunk_size: int, overlap: int = 0) -> List[str]:
347
+ """Split text into overlapping chunks"""
348
+ chunks = []
349
+ start = 0
350
+
351
+ while start < len(text):
352
+ end = start + chunk_size
353
+ chunks.append(text[start:end])
354
+ start = end - overlap
355
+
356
+ return chunks
357
+
358
+
359
+ def get_project_root() -> Path:
360
+ """Get project root directory"""
361
+ return Path(__file__).parent.parent
362
+
363
+
364
+ def ensure_dir(path: str):
365
+ """Ensure directory exists"""
366
+ Path(path).mkdir(parents=True, exist_ok=True)