apirrone commited on
Commit
bfbe08d
·
1 Parent(s): 927fd13

starting to appify

Browse files
Files changed (5) hide show
  1. README.md +11 -0
  2. index.html +280 -0
  3. pyproject.toml +3 -0
  4. src/reachy_mini_conversation_demo/main.py +16 -3
  5. style.css +411 -0
README.md CHANGED
@@ -1,3 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
1
  # Reachy Mini conversation demo
2
 
3
  Conversational demo for the Reachy Mini robot combining OpenAI's realtime APIs, vision pipelines, and choreographed motion libraries.
 
1
+ ---
2
+ title: Reachy Mini Conversation Demo
3
+ emoji: 🎤
4
+ colorFrom: red
5
+ colorTo: blue
6
+ sdk: static
7
+ pinned: false
8
+ tags:
9
+ - reachy_mini
10
+ ---
11
+
12
  # Reachy Mini conversation demo
13
 
14
  Conversational demo for the Reachy Mini robot combining OpenAI's realtime APIs, vision pipelines, and choreographed motion libraries.
index.html ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width" />
7
+ <title>Reachy Mini Conversation Demo</title>
8
+ <link rel="stylesheet" href="style.css" />
9
+ </head>
10
+
11
+ <body>
12
+ <div class="hero">
13
+ <div class="hero-content">
14
+ <div class="app-icon">🤖⚡</div>
15
+ <h1>Reachy Mini Conversation Demo</h1>
16
+ <p class="tagline">Conversational demo for the Reachy Mini robot combining OpenAI's realtime APIs, vision pipelines, and choreographed motion libraries.</p>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="container">
21
+ <div class="main-card">
22
+ <div class="app-preview">
23
+ <div class="preview-image">
24
+ <div class="camera-feed">🛠️</div>
25
+ </div>
26
+ </div>
27
+ </div>
28
+
29
+ <div class="app-details">
30
+ <h2>Example Template App</h2>
31
+ <div class="template-info">
32
+ <div class="info-box">
33
+ <h3>🎨 Template Purpose</h3>
34
+ <p>This is an example landing page for Reachy Mini apps. Feel free to duplicate this template and
35
+ customize it for your own applications!</p>
36
+ </div>
37
+ <div class="info-box">
38
+ <h3>🚀 Getting Started</h3>
39
+ <p>Use this template to showcase your Reachy Mini app with a landing page. Simply modify the
40
+ content, add your app's repository URL, and deploy!</p>
41
+ </div>
42
+ </div>
43
+
44
+
45
+ <div class="how-to-use">
46
+ <h3>How to Use This Template</h3>
47
+ <div class="steps">
48
+ <div class="step">
49
+ <span class="step-number">1</span>
50
+ <div>
51
+ <h4>Duplicate & Customize</h4>
52
+ <p>Copy this template and modify the content for your app</p>
53
+ </div>
54
+ </div>
55
+ <div class="step">
56
+ <span class="step-number">2</span>
57
+ <div>
58
+ <h4>Update Repository URL</h4>
59
+ <p>Change the JavaScript to point to your app's Git repository</p>
60
+ </div>
61
+ </div>
62
+ <div class="step">
63
+ <span class="step-number">3</span>
64
+ <div>
65
+ <h4>Deploy to HF Spaces</h4>
66
+ <p>Upload your customized version to Hugging Face Spaces</p>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <div class="download-section">
75
+ <div class="download-card">
76
+ <h2>Install This App</h2>
77
+ <p></p>
78
+
79
+ <div class="dashboard-config">
80
+ <label for="dashboardUrl">Your Reachy Dashboard URL:</label>
81
+ <input type="url" id="dashboardUrl" value="http://localhost:8000"
82
+ placeholder="http://your-reachy-ip:8000" />
83
+ </div>
84
+
85
+ <button id="installBtn" class="install-btn primary">
86
+ <span class="btn-icon">📥</span>
87
+ Install Conversation Demo to Reachy Mini
88
+ </button>
89
+
90
+ <div id="installStatus" class="install-status"></div>
91
+
92
+ </div>
93
+ </div>
94
+
95
+ <div class="footer">
96
+ <p>
97
+ 🤖 Template for Reachy Mini Apps •
98
+ <a href="https://github.com/pollen-robotics" target="_blank">Pollen Robotics</a> •
99
+ <a href="https://huggingface.co/spaces/pollen-robotics/Reachy_Mini_Apps" target="_blank">Browse More
100
+ Apps</a>
101
+ </p>
102
+ </div>
103
+ </div>
104
+
105
+ <script>
106
+ // Get the current Hugging Face Space URL as the repository URL
107
+ function getCurrentSpaceUrl() {
108
+ // Get current page URL and convert to repository format
109
+ const currentUrl = window.location.href;
110
+
111
+ // Remove any trailing slashes and query parameters
112
+ const cleanUrl = currentUrl.split('?')[0].replace(/\/$/, '');
113
+
114
+ return cleanUrl;
115
+ }
116
+
117
+ // Parse TOML content to extract project name
118
+ function parseTomlProjectName(tomlContent) {
119
+ try {
120
+ const lines = tomlContent.split('\n');
121
+ let inProjectSection = false;
122
+
123
+ for (const line of lines) {
124
+ const trimmedLine = line.trim();
125
+
126
+ // Check if we're entering the [project] section
127
+ if (trimmedLine === '[project]') {
128
+ inProjectSection = true;
129
+ continue;
130
+ }
131
+
132
+ // Check if we're entering a different section
133
+ if (trimmedLine.startsWith('[') && trimmedLine !== '[project]') {
134
+ inProjectSection = false;
135
+ continue;
136
+ }
137
+
138
+ // If we're in the project section, look for the name field
139
+ if (inProjectSection && trimmedLine.startsWith('name')) {
140
+ const match = trimmedLine.match(/name\s*=\s*["']([^"']+)["']/);
141
+ if (match) {
142
+ // Convert to lowercase and replace invalid characters for app naming
143
+ return match[1].toLowerCase().replace(/[^a-z0-9-_]/g, '-');
144
+ }
145
+ }
146
+ }
147
+
148
+ throw new Error('Project name not found in pyproject.toml');
149
+ } catch (error) {
150
+ console.error('Error parsing pyproject.toml:', error);
151
+ return 'unknown-app';
152
+ }
153
+ }
154
+
155
+ // Fetch and parse pyproject.toml from the current space
156
+ async function getAppNameFromCurrentSpace() {
157
+ try {
158
+ // Fetch pyproject.toml from the current space
159
+ const response = await fetch('./pyproject.toml');
160
+ if (!response.ok) {
161
+ throw new Error(`Failed to fetch pyproject.toml: ${response.status}`);
162
+ }
163
+
164
+ const tomlContent = await response.text();
165
+ return parseTomlProjectName(tomlContent);
166
+ } catch (error) {
167
+ console.error('Error fetching app name from current space:', error);
168
+ // Fallback to extracting from URL if pyproject.toml is not accessible
169
+ const url = getCurrentSpaceUrl();
170
+ const parts = url.split('/');
171
+ const spaceName = parts[parts.length - 1];
172
+ return spaceName.toLowerCase().replace(/[^a-z0-9-_]/g, '-');
173
+ }
174
+ }
175
+
176
+ async function installToReachy() {
177
+ const dashboardUrl = document.getElementById('dashboardUrl').value.trim();
178
+ const statusDiv = document.getElementById('installStatus');
179
+ const installBtn = document.getElementById('installBtn');
180
+
181
+ if (!dashboardUrl) {
182
+ showStatus('error', 'Please enter your Reachy dashboard URL');
183
+ return;
184
+ }
185
+
186
+ try {
187
+ installBtn.disabled = true;
188
+ installBtn.innerHTML = '<span class="btn-icon">⏳</span>Installing...';
189
+ showStatus('loading', 'Connecting to your Reachy dashboard...');
190
+
191
+ // Test connection
192
+ const testResponse = await fetch(`${dashboardUrl}/api/status`, {
193
+ method: 'GET',
194
+ mode: 'cors',
195
+ });
196
+
197
+ if (!testResponse.ok) {
198
+ throw new Error('Cannot connect to dashboard. Make sure the URL is correct and the dashboard is running.');
199
+ }
200
+
201
+ showStatus('loading', 'Reading app configuration...');
202
+
203
+ // Get app name from pyproject.toml in current space
204
+ const appName = await getAppNameFromCurrentSpace();
205
+
206
+ // Get current space URL as repository URL
207
+ const repoUrl = getCurrentSpaceUrl();
208
+
209
+ showStatus('loading', `Starting installation of "${appName}"...`);
210
+
211
+ // Start installation
212
+ const installResponse = await fetch(`${dashboardUrl}/api/install`, {
213
+ method: 'POST',
214
+ mode: 'cors',
215
+ headers: {
216
+ 'Content-Type': 'application/json',
217
+ },
218
+ body: JSON.stringify({
219
+ url: repoUrl,
220
+ name: appName
221
+ })
222
+ });
223
+
224
+ const result = await installResponse.json();
225
+
226
+ if (installResponse.ok) {
227
+ showStatus('success', `✅ Installation started for "${appName}"! Check your dashboard for progress.`);
228
+ setTimeout(() => {
229
+ showStatus('info', `Open your dashboard at ${dashboardUrl} to see the installed app.`);
230
+ }, 3000);
231
+ } else {
232
+ throw new Error(result.detail || 'Installation failed');
233
+ }
234
+
235
+ } catch (error) {
236
+ console.error('Installation error:', error);
237
+ showStatus('error', `❌ ${error.message}`);
238
+ } finally {
239
+ installBtn.disabled = false;
240
+ installBtn.innerHTML = '<span class="btn-icon">📥</span>Install App to Reachy';
241
+ }
242
+ }
243
+
244
+ function showStatus(type, message) {
245
+ const statusDiv = document.getElementById('installStatus');
246
+ statusDiv.className = `install-status ${type}`;
247
+ statusDiv.textContent = message;
248
+ statusDiv.style.display = 'block';
249
+ }
250
+
251
+ function copyToClipboard() {
252
+ const repoUrl = document.getElementById('repoUrl').textContent;
253
+ navigator.clipboard.writeText(repoUrl).then(() => {
254
+ showStatus('success', '📋 Repository URL copied to clipboard!');
255
+ }).catch(() => {
256
+ showStatus('error', 'Failed to copy URL. Please copy manually.');
257
+ });
258
+ }
259
+
260
+ // Update the displayed repository URL on page load
261
+ document.addEventListener('DOMContentLoaded', () => {
262
+ // Auto-detect local dashboard
263
+ const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
264
+ if (isLocalhost) {
265
+ document.getElementById('dashboardUrl').value = 'http://localhost:8000';
266
+ }
267
+
268
+ // Update the repository URL display if element exists
269
+ const repoUrlElement = document.getElementById('repoUrl');
270
+ if (repoUrlElement) {
271
+ repoUrlElement.textContent = getCurrentSpaceUrl();
272
+ }
273
+ });
274
+
275
+ // Event listeners
276
+ document.getElementById('installBtn').addEventListener('click', installToReachy);
277
+ </script>
278
+ </body>
279
+
280
+ </html>
pyproject.toml CHANGED
@@ -45,6 +45,9 @@ dev = ["pytest", "ruff==0.12.0"]
45
  [project.scripts]
46
  reachy-mini-conversation-demo = "reachy_mini_conversation_demo.main:main"
47
 
 
 
 
48
  [tool.setuptools]
49
  package-dir = { "" = "src" }
50
  include-package-data = true
 
45
  [project.scripts]
46
  reachy-mini-conversation-demo = "reachy_mini_conversation_demo.main:main"
47
 
48
+ [project.entry-points."reachy_mini_apps"]
49
+ reachy_mini_conversation_demo = "reachy_mini_conversation_demo.main:ReachyMiniConversationDemo"
50
+
51
  [tool.setuptools]
52
  package-dir = { "" = "src" }
53
  include-package-data = true
src/reachy_mini_conversation_demo/main.py CHANGED
@@ -2,12 +2,14 @@
2
 
3
  import os
4
  import sys
 
 
5
 
6
  import gradio as gr
7
  from fastapi import FastAPI
8
  from fastrtc import Stream
9
 
10
- from reachy_mini import ReachyMini
11
  from reachy_mini_conversation_demo.moves import MovementManager
12
  from reachy_mini_conversation_demo.tools import ToolDependencies
13
  from reachy_mini_conversation_demo.utils import (
@@ -26,7 +28,7 @@ def update_chatbot(chatbot: list[dict], response: dict):
26
  return chatbot
27
 
28
 
29
- def main():
30
  """Entrypoint for the Reachy Mini conversation demo."""
31
  args = parse_args()
32
 
@@ -36,7 +38,8 @@ def main():
36
  if args.no_camera and args.head_tracker is not None:
37
  logger.warning("Head tracking is not activated due to --no-camera.")
38
 
39
- robot = ReachyMini()
 
40
 
41
  # Check if running in simulation mode without --gradio
42
  if robot.client.get_status()["simulation_enabled"] and not args.gradio:
@@ -123,5 +126,15 @@ def main():
123
  logger.info("Shutdown complete.")
124
 
125
 
 
 
 
 
 
 
 
 
 
 
126
  if __name__ == "__main__":
127
  main()
 
2
 
3
  import os
4
  import sys
5
+ import time
6
+ import threading
7
 
8
  import gradio as gr
9
  from fastapi import FastAPI
10
  from fastrtc import Stream
11
 
12
+ from reachy_mini import ReachyMini, ReachyMiniApp
13
  from reachy_mini_conversation_demo.moves import MovementManager
14
  from reachy_mini_conversation_demo.tools import ToolDependencies
15
  from reachy_mini_conversation_demo.utils import (
 
28
  return chatbot
29
 
30
 
31
+ def main(robot=None):
32
  """Entrypoint for the Reachy Mini conversation demo."""
33
  args = parse_args()
34
 
 
38
  if args.no_camera and args.head_tracker is not None:
39
  logger.warning("Head tracking is not activated due to --no-camera.")
40
 
41
+ if robot is None:
42
+ robot = ReachyMini()
43
 
44
  # Check if running in simulation mode without --gradio
45
  if robot.client.get_status()["simulation_enabled"] and not args.gradio:
 
126
  logger.info("Shutdown complete.")
127
 
128
 
129
+ class ReachyMiniConversationDemo(ReachyMiniApp):
130
+ """Reachy Mini Apps entry point for the conversation demo."""
131
+
132
+ def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
133
+ """Run the Reachy Mini conversation demo app."""
134
+ while not stop_event.is_set():
135
+ main(robot=reachy_mini)
136
+ time.sleep(1)
137
+
138
+
139
  if __name__ == "__main__":
140
  main()
style.css ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
9
+ line-height: 1.6;
10
+ color: #333;
11
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12
+ min-height: 100vh;
13
+ }
14
+
15
+ .hero {
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ color: white;
18
+ padding: 4rem 2rem;
19
+ text-align: center;
20
+ }
21
+
22
+ .hero-content {
23
+ max-width: 800px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ .app-icon {
28
+ font-size: 4rem;
29
+ margin-bottom: 1rem;
30
+ display: inline-block;
31
+ }
32
+
33
+ .hero h1 {
34
+ font-size: 3rem;
35
+ font-weight: 700;
36
+ margin-bottom: 1rem;
37
+ background: linear-gradient(45deg, #fff, #f0f9ff);
38
+ background-clip: text;
39
+ -webkit-background-clip: text;
40
+ -webkit-text-fill-color: transparent;
41
+ }
42
+
43
+ .tagline {
44
+ font-size: 1.25rem;
45
+ opacity: 0.9;
46
+ max-width: 600px;
47
+ margin: 0 auto;
48
+ }
49
+
50
+ .container {
51
+ max-width: 1200px;
52
+ margin: 0 auto;
53
+ padding: 0 2rem;
54
+ position: relative;
55
+ z-index: 2;
56
+ }
57
+
58
+ .main-card {
59
+ background: white;
60
+ border-radius: 20px;
61
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
62
+ margin-top: -2rem;
63
+ overflow: hidden;
64
+ margin-bottom: 3rem;
65
+ }
66
+
67
+ .app-preview {
68
+ background: linear-gradient(135deg, #1e3a8a, #3b82f6);
69
+ padding: 3rem;
70
+ color: white;
71
+ text-align: center;
72
+ position: relative;
73
+ }
74
+
75
+ .preview-image {
76
+ background: #000;
77
+ border-radius: 15px;
78
+ padding: 2rem;
79
+ max-width: 500px;
80
+ margin: 0 auto;
81
+ position: relative;
82
+ overflow: hidden;
83
+ }
84
+
85
+ .camera-feed {
86
+ font-size: 4rem;
87
+ margin-bottom: 1rem;
88
+ opacity: 0.7;
89
+ }
90
+
91
+ .detection-overlay {
92
+ position: absolute;
93
+ top: 50%;
94
+ left: 50%;
95
+ transform: translate(-50%, -50%);
96
+ width: 100%;
97
+ }
98
+
99
+ .bbox {
100
+ background: rgba(34, 197, 94, 0.9);
101
+ color: white;
102
+ padding: 0.5rem 1rem;
103
+ border-radius: 8px;
104
+ font-size: 0.9rem;
105
+ font-weight: 600;
106
+ margin: 0.5rem;
107
+ display: inline-block;
108
+ border: 2px solid #22c55e;
109
+ }
110
+
111
+ .app-details {
112
+ padding: 3rem;
113
+ }
114
+
115
+ .app-details h2 {
116
+ font-size: 2rem;
117
+ color: #1e293b;
118
+ margin-bottom: 2rem;
119
+ text-align: center;
120
+ }
121
+
122
+ .template-info {
123
+ display: grid;
124
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
125
+ gap: 2rem;
126
+ margin-bottom: 3rem;
127
+ }
128
+
129
+ .info-box {
130
+ background: #f0f9ff;
131
+ border: 2px solid #e0f2fe;
132
+ border-radius: 12px;
133
+ padding: 2rem;
134
+ }
135
+
136
+ .info-box h3 {
137
+ color: #0c4a6e;
138
+ margin-bottom: 1rem;
139
+ font-size: 1.2rem;
140
+ }
141
+
142
+ .info-box p {
143
+ color: #0369a1;
144
+ line-height: 1.6;
145
+ }
146
+
147
+ .how-to-use {
148
+ background: #fefce8;
149
+ border: 2px solid #fde047;
150
+ border-radius: 12px;
151
+ padding: 2rem;
152
+ margin-top: 3rem;
153
+ }
154
+
155
+ .how-to-use h3 {
156
+ color: #a16207;
157
+ margin-bottom: 1.5rem;
158
+ font-size: 1.3rem;
159
+ text-align: center;
160
+ }
161
+
162
+ .steps {
163
+ display: flex;
164
+ flex-direction: column;
165
+ gap: 1.5rem;
166
+ }
167
+
168
+ .step {
169
+ display: flex;
170
+ align-items: flex-start;
171
+ gap: 1rem;
172
+ }
173
+
174
+ .step-number {
175
+ background: #eab308;
176
+ color: white;
177
+ width: 2rem;
178
+ height: 2rem;
179
+ border-radius: 50%;
180
+ display: flex;
181
+ align-items: center;
182
+ justify-content: center;
183
+ font-weight: bold;
184
+ flex-shrink: 0;
185
+ }
186
+
187
+ .step h4 {
188
+ color: #a16207;
189
+ margin-bottom: 0.5rem;
190
+ font-size: 1.1rem;
191
+ }
192
+
193
+ .step p {
194
+ color: #ca8a04;
195
+ }
196
+
197
+ .download-card {
198
+ background: white;
199
+ border-radius: 20px;
200
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
201
+ padding: 3rem;
202
+ text-align: center;
203
+ }
204
+
205
+ .download-card h2 {
206
+ font-size: 2rem;
207
+ color: #1e293b;
208
+ margin-bottom: 1rem;
209
+ }
210
+
211
+ .download-card>p {
212
+ color: #64748b;
213
+ font-size: 1.1rem;
214
+ margin-bottom: 2rem;
215
+ }
216
+
217
+ .dashboard-config {
218
+ margin-bottom: 2rem;
219
+ text-align: left;
220
+ max-width: 400px;
221
+ margin-left: auto;
222
+ margin-right: auto;
223
+ }
224
+
225
+ .dashboard-config label {
226
+ display: block;
227
+ color: #374151;
228
+ font-weight: 600;
229
+ margin-bottom: 0.5rem;
230
+ }
231
+
232
+ .dashboard-config input {
233
+ width: 100%;
234
+ padding: 0.75rem 1rem;
235
+ border: 2px solid #e5e7eb;
236
+ border-radius: 8px;
237
+ font-size: 0.95rem;
238
+ transition: border-color 0.2s;
239
+ }
240
+
241
+ .dashboard-config input:focus {
242
+ outline: none;
243
+ border-color: #667eea;
244
+ }
245
+
246
+ .install-btn {
247
+ background: linear-gradient(135deg, #667eea, #764ba2);
248
+ color: white;
249
+ border: none;
250
+ padding: 1.25rem 3rem;
251
+ border-radius: 16px;
252
+ font-size: 1.2rem;
253
+ font-weight: 700;
254
+ cursor: pointer;
255
+ transition: all 0.3s ease;
256
+ display: inline-flex;
257
+ align-items: center;
258
+ gap: 0.75rem;
259
+ margin-bottom: 2rem;
260
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
261
+ }
262
+
263
+ .install-btn:hover:not(:disabled) {
264
+ transform: translateY(-3px);
265
+ box-shadow: 0 15px 35px rgba(102, 126, 234, 0.4);
266
+ }
267
+
268
+ .install-btn:disabled {
269
+ opacity: 0.7;
270
+ cursor: not-allowed;
271
+ transform: none;
272
+ }
273
+
274
+ .manual-option {
275
+ background: #f8fafc;
276
+ border-radius: 12px;
277
+ padding: 2rem;
278
+ margin-top: 2rem;
279
+ }
280
+
281
+ .manual-option h3 {
282
+ color: #1e293b;
283
+ margin-bottom: 1rem;
284
+ font-size: 1.2rem;
285
+ }
286
+
287
+ .manual-option>p {
288
+ color: #64748b;
289
+ margin-bottom: 1rem;
290
+ }
291
+
292
+ .btn-icon {
293
+ font-size: 1.1rem;
294
+ }
295
+
296
+ .install-status {
297
+ padding: 1rem;
298
+ border-radius: 8px;
299
+ font-size: 0.9rem;
300
+ text-align: center;
301
+ display: none;
302
+ margin-top: 1rem;
303
+ }
304
+
305
+ .install-status.success {
306
+ background: #dcfce7;
307
+ color: #166534;
308
+ border: 1px solid #bbf7d0;
309
+ }
310
+
311
+ .install-status.error {
312
+ background: #fef2f2;
313
+ color: #dc2626;
314
+ border: 1px solid #fecaca;
315
+ }
316
+
317
+ .install-status.loading {
318
+ background: #dbeafe;
319
+ color: #1d4ed8;
320
+ border: 1px solid #bfdbfe;
321
+ }
322
+
323
+ .install-status.info {
324
+ background: #e0f2fe;
325
+ color: #0369a1;
326
+ border: 1px solid #7dd3fc;
327
+ }
328
+
329
+ .manual-install {
330
+ background: #1f2937;
331
+ border-radius: 8px;
332
+ padding: 1rem;
333
+ margin-bottom: 1rem;
334
+ display: flex;
335
+ align-items: center;
336
+ gap: 1rem;
337
+ }
338
+
339
+ .manual-install code {
340
+ color: #10b981;
341
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
342
+ font-size: 0.85rem;
343
+ flex: 1;
344
+ overflow-x: auto;
345
+ }
346
+
347
+ .copy-btn {
348
+ background: #374151;
349
+ color: white;
350
+ border: none;
351
+ padding: 0.5rem 1rem;
352
+ border-radius: 6px;
353
+ font-size: 0.8rem;
354
+ cursor: pointer;
355
+ transition: background-color 0.2s;
356
+ }
357
+
358
+ .copy-btn:hover {
359
+ background: #4b5563;
360
+ }
361
+
362
+ .manual-steps {
363
+ color: #6b7280;
364
+ font-size: 0.9rem;
365
+ line-height: 1.8;
366
+ }
367
+
368
+ .footer {
369
+ text-align: center;
370
+ padding: 2rem;
371
+ color: white;
372
+ opacity: 0.8;
373
+ }
374
+
375
+ .footer a {
376
+ color: white;
377
+ text-decoration: none;
378
+ font-weight: 600;
379
+ }
380
+
381
+ .footer a:hover {
382
+ text-decoration: underline;
383
+ }
384
+
385
+ /* Responsive Design */
386
+ @media (max-width: 768px) {
387
+ .hero {
388
+ padding: 2rem 1rem;
389
+ }
390
+
391
+ .hero h1 {
392
+ font-size: 2rem;
393
+ }
394
+
395
+ .container {
396
+ padding: 0 1rem;
397
+ }
398
+
399
+ .app-details,
400
+ .download-card {
401
+ padding: 2rem;
402
+ }
403
+
404
+ .features-grid {
405
+ grid-template-columns: 1fr;
406
+ }
407
+
408
+ .download-options {
409
+ grid-template-columns: 1fr;
410
+ }
411
+ }